Use on-demand loading for results in the query tool. Fixes #2137

With a 27420 row query, pgAdmin III runs the query in 5.873s on my laptop. pgAdmin 4 now takes ~1s.
This commit is contained in:
Harshal Dhumal 2017-06-27 09:03:04 -04:00 committed by Dave Page
parent 15cb9fc35b
commit c65158312d
28 changed files with 1953 additions and 887 deletions

View File

@ -321,6 +321,12 @@ THREADED_MODE = True
##########################################################################
SQLALCHEMY_TRACK_MODIFICATIONS = False
##########################################################################
# Number of records to fetch in one batch in query tool when query result
# set is large.
##########################################################################
ON_DEMAND_RECORD_COUNT = 1000
##########################################################################
# Local config settings
##########################################################################

View File

@ -7,6 +7,7 @@
#
##########################################################################
import time
from selenium.webdriver import ActionChains
import config as app_config
@ -53,6 +54,7 @@ class ConnectsToServerFeatureTest(BaseFeatureTest):
def _connects_to_server(self):
self.page.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click()
time.sleep(2)
self.page.driver.find_element_by_link_text("Object").click()
ActionChains(self.page.driver) \
.move_to_element(self.page.driver.find_element_by_link_text("Create")) \
@ -72,6 +74,8 @@ class ConnectsToServerFeatureTest(BaseFeatureTest):
self.page.toggle_open_server(self.server['name'])
self.page.toggle_open_tree_item('Databases')
self.page.toggle_open_tree_item('acceptance_test_db')
# wait until all database dependant modules/js are loaded.
time.sleep(5)
self.page.toggle_open_tree_item('Schemas')
self.page.toggle_open_tree_item('public')
self.page.toggle_open_tree_item('Tables')

View File

@ -6,6 +6,7 @@
# This software is released under the PostgreSQL Licence
#
##########################################################################
import time
from selenium.webdriver import ActionChains
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.support.ui import WebDriverWait
@ -56,6 +57,7 @@ class PGDataypeFeatureTest(BaseFeatureTest):
self.page.find_by_xpath(
"//*[@class='aciTreeText' and .='Servers']"
).click()
time.sleep(2)
self.page.driver.find_element_by_link_text("Object").click()
ActionChains(self.page.driver) \
.move_to_element(
@ -106,45 +108,19 @@ class PGDataypeFeatureTest(BaseFeatureTest):
self.page.fill_codemirror_area_with(query)
self.page.find_by_id("btn-flash").click()
wait = WebDriverWait(self.page.driver, 5)
wait.until(EC.presence_of_element_located(
(By.XPATH, "//*[@id='0']//*[@id='datagrid']/div[5]/div/div[1]/"
"div[2]/span")))
canvas = wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, "#datagrid .slick-viewport .grid-canvas"))
)
# For every sample data-type value, check the expected output.
cnt = 2
for val in expected_output[:10]:
cells = canvas.find_elements_by_css_selector('.slick-cell')
# remove first element as it is row number.
cells.pop(0)
for val, cell in zip(expected_output, cells):
try:
source_code = self.page.find_by_xpath(
"//*[@id='0']//*[@id='datagrid']/div[5]/div/div[1]/div["
+ str(cnt)
+ "]/span"
).get_attribute('innerHTML')
PGDataypeFeatureTest.check_result(
source_code,
expected_output[cnt - 2]
)
cnt += 1
except TimeoutException:
assert False, "{0} does not match with {1}".format(
val, expected_output[cnt]
)
cnt = 12
for val in expected_output[10:]:
try:
if cnt == 14:
xpath = "//*[@id='0']//*[@id='datagrid']/div[5]/div/div[1]/div[" \
+ str(cnt) \
+ "]/span"
else:
xpath = "//*[@id='0']//*[@id='datagrid']/div[5]/div/div/div[" \
+ str(cnt) \
+ "]"
source_code = self.page.find_by_xpath(
xpath
).get_attribute('innerHTML')
source_code = cell.get_attribute('innerHTML')
PGDataypeFeatureTest.check_result(
source_code,

View File

@ -0,0 +1,734 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2017, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
from __future__ import print_function
import time
import sys
import config
from selenium.webdriver import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from regression.python_test_utils import test_utils
from regression.feature_utils.base_feature_test import BaseFeatureTest
class QueryToolFeatureTest(BaseFeatureTest):
"""
This feature test will test the different query tool features.
"""
scenarios = [
("Query tool feature test", dict())
]
def before(self):
connection = test_utils.get_db_connection(self.server['db'],
self.server['username'],
self.server['db_password'],
self.server['host'],
self.server['port'])
test_utils.drop_database(connection, "acceptance_test_db")
test_utils.create_database(self.server, "acceptance_test_db")
self.page.wait_for_spinner_to_disappear()
self._connects_to_server()
self._locate_database_tree_node()
self.page.open_query_tool()
def runTest(self):
# on demand result set on scrolling.
print("\nOn demand result set on scrolling... ",
file=sys.stderr, end="")
self._on_demand_result()
print("OK.",
file=sys.stderr)
self._clear_query_tool()
# on demand result set on grid select all.
print("On demand result set on grid select all... ",
file=sys.stderr, end="")
self._on_demand_result_select_all_grid()
print("OK.",
file=sys.stderr)
self._clear_query_tool()
# on demand result set on column select all.
print("On demand result set on column select all... ",
file=sys.stderr, end="")
self._on_demand_result_select_all_column()
print("OK.",
file=sys.stderr)
self._clear_query_tool()
# explain query
print("Explain query... ", file=sys.stderr, end="")
self._query_tool_explain()
print("OK.", file=sys.stderr)
self._clear_query_tool()
# explain query with verbose
print("Explain query with verbose... ", file=sys.stderr, end="")
self._query_tool_explain_verbose()
print("OK.", file=sys.stderr)
self._clear_query_tool()
# explain query with costs
print("Explain query with costs... ", file=sys.stderr, end="")
self._query_tool_explain_cost()
print("OK.", file=sys.stderr)
self._clear_query_tool()
# explain analyze query
print("Explain analyze query... ", file=sys.stderr, end="")
self._query_tool_explain_analyze()
print("OK.", file=sys.stderr)
self._clear_query_tool()
# explain analyze query with buffers
print("Explain analyze query with buffers... ", file=sys.stderr, end="")
self._query_tool_explain_analyze_buffers()
print("OK.", file=sys.stderr)
self._clear_query_tool()
# explain analyze query with timing
print("Explain analyze query with timing... ", file=sys.stderr, end="")
self._query_tool_explain_analyze_timing()
print("OK.", file=sys.stderr)
self._clear_query_tool()
# auto commit disabled.
print("Auto commit disabled... ", file=sys.stderr, end="")
self._query_tool_auto_commit_disabled()
print("OK.", file=sys.stderr)
self._clear_query_tool()
# auto commit enabled.
print("Auto commit enabled... ", file=sys.stderr, end="")
self._query_tool_auto_commit_enabled()
print("OK.", file=sys.stderr)
self._clear_query_tool()
# auto rollback enabled.
print("Auto rollback enabled...", file=sys.stderr, end="")
self._query_tool_auto_rollback_enabled()
print(" OK.", file=sys.stderr)
self._clear_query_tool()
# cancel query.
print("Cancel query... ", file=sys.stderr, end="")
self._query_tool_cancel_query()
print("OK.", file=sys.stderr)
self._clear_query_tool()
def after(self):
self.page.remove_server(self.server)
connection = test_utils.get_db_connection(self.server['db'],
self.server['username'],
self.server['db_password'],
self.server['host'],
self.server['port'])
test_utils.drop_database(connection, "acceptance_test_db")
def _connects_to_server(self):
self.page.find_by_xpath(
"//*[@class='aciTreeText' and .='Servers']").click()
time.sleep(2)
self.page.driver.find_element_by_link_text("Object").click()
ActionChains(self.page.driver) \
.move_to_element(
self.page.driver.find_element_by_link_text("Create"))\
.perform()
self.page.find_by_partial_link_text("Server...").click()
server_config = self.server
self.page.fill_input_by_field_name("name", server_config['name'])
self.page.find_by_partial_link_text("Connection").click()
self.page.fill_input_by_field_name("host", server_config['host'])
self.page.fill_input_by_field_name("port", server_config['port'])
self.page.fill_input_by_field_name(
"username",
server_config['username']
)
self.page.fill_input_by_field_name(
"password",
server_config['db_password']
)
self.page.find_by_xpath("//button[contains(.,'Save')]").click()
def _locate_database_tree_node(self):
self.page.toggle_open_tree_item(self.server['name'])
self.page.toggle_open_tree_item('Databases')
self.page.toggle_open_tree_item('acceptance_test_db')
def _clear_query_tool(self):
# clear codemirror.
self.page.find_by_id("btn-edit").click()
# wait for alertify dialog open animation to complete.
time.sleep(1)
self.page.click_element(self.page.find_by_xpath("//button[contains(.,'Yes')]"))
# wait for alertify dialog close animation to complete.
time.sleep(1)
def _on_demand_result(self):
ON_DEMAND_CHUNKS = 2
query = """-- On demand query result on scroll
SELECT generate_series(1, {}) as id""".format(
config.ON_DEMAND_RECORD_COUNT * ON_DEMAND_CHUNKS)
wait = WebDriverWait(self.page.driver, 10)
self.page.fill_codemirror_area_with(query)
self.page.find_by_id("btn-flash").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
canvas = wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, "#datagrid .slick-viewport .grid-canvas")))
# scroll to bottom to fetch next chunk of result set.
self.driver.execute_script(
"$('.slick-viewport').scrollTop($('.grid-canvas').height());"
)
# wait for ajax to complete.
time.sleep(1)
# again scroll to bottom to bring last row of next chunk in
# viewport.
self.driver.execute_script(
"$('.slick-viewport').scrollTop($('.grid-canvas').height());"
)
row_id_to_find = config.ON_DEMAND_RECORD_COUNT * ON_DEMAND_CHUNKS
canvas.find_element_by_xpath(
'//span[text()="{}"]'.format(row_id_to_find)
)
def _on_demand_result_select_all_grid(self):
ON_DEMAND_CHUNKS = 3
query = """-- On demand query result on grid select all
SELECT generate_series(1, {}) as id""".format(
config.ON_DEMAND_RECORD_COUNT * ON_DEMAND_CHUNKS)
wait = WebDriverWait(self.page.driver, 10)
self.page.fill_codemirror_area_with(query)
self.page.find_by_id("btn-flash").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, ".slick-header-column"))).click()
# wait for until all records are fetched and selected.
time.sleep(1)
# scroll to bottom to bring last row of next chunk in
# viewport.
self.driver.execute_script(
"$('.slick-viewport').scrollTop($('.grid-canvas').height());"
)
canvas = wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, "#datagrid .slick-viewport .grid-canvas"))
)
row_id_to_find = config.ON_DEMAND_RECORD_COUNT * ON_DEMAND_CHUNKS
canvas.find_element_by_xpath(
'//span[text()="{}"]'.format(row_id_to_find)
)
def _on_demand_result_select_all_column(self):
ON_DEMAND_CHUNKS = 4
query = """-- On demand query result on column select all
SELECT generate_series(1, {}) as id1, 'dummy' as id2""".format(
config.ON_DEMAND_RECORD_COUNT * ON_DEMAND_CHUNKS)
wait = WebDriverWait(self.page.driver, 10)
self.page.fill_codemirror_area_with(query)
self.page.find_by_id("btn-flash").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
# click on first data column to select all column.
wait.until(EC.presence_of_element_located(
(
By.XPATH,
"//span[contains(@class, 'column-name') and contains(., 'id1')]"))
).click()
# wait for until all records are fetched and selected.
time.sleep(1)
# scroll to bottom to bring last row of next chunk in
# viewport.
self.driver.execute_script(
"$('.slick-viewport').scrollTop($('.grid-canvas').height());"
)
canvas = wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, "#datagrid .slick-viewport .grid-canvas"))
)
row_id_to_find = config.ON_DEMAND_RECORD_COUNT * ON_DEMAND_CHUNKS
canvas.find_element_by_xpath(
'//span[text()="{}"]'.format(row_id_to_find)
)
def _query_tool_explain(self):
query = """-- Explain query
SELECT generate_series(1, 1000) as id order by id desc"""
wait = WebDriverWait(self.page.driver, 10)
self.page.fill_codemirror_area_with(query)
self.page.find_by_id("btn-query-dropdown").click()
self.page.find_by_id("btn-explain").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Data Output')
canvas = wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, "#datagrid .slick-viewport .grid-canvas"))
)
# Search for Plan word in result
canvas.find_element_by_xpath("//*[contains(string(),'Plan')]")
def _query_tool_explain_verbose(self):
query = """-- Explain query with verbose
SELECT generate_series(1, 1000) as id order by id desc"""
wait = WebDriverWait(self.page.driver, 10)
self.page.fill_codemirror_area_with(query)
query_op = self.page.find_by_id("btn-query-dropdown")
query_op.click()
ActionChains(self.driver).move_to_element(
query_op.find_element_by_xpath(
"//li[contains(.,'Explain Options')]")).perform()
self.page.find_by_id("btn-explain-verbose").click()
self.page.find_by_id("btn-explain").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Data Output')
canvas = wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, "#datagrid .slick-viewport .grid-canvas"))
)
# Search for 'Output' word in result
canvas.find_element_by_xpath("//*[contains(string(), 'Output')]")
def _query_tool_explain_cost(self):
query = """-- Explain query with costs
SELECT generate_series(1, 1000) as id order by id desc"""
wait = WebDriverWait(self.page.driver, 10)
self.page.fill_codemirror_area_with(query)
query_op = self.page.find_by_id("btn-query-dropdown")
query_op.click()
ActionChains(self.driver).move_to_element(
query_op.find_element_by_xpath(
"//li[contains(.,'Explain Options')]")).perform()
self.page.find_by_id("btn-explain-costs").click()
self.page.find_by_id("btn-explain").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Data Output')
canvas = wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, "#datagrid .slick-viewport .grid-canvas"))
)
# Search for 'Total Cost word in result
canvas.find_element_by_xpath("//*[contains(string(),'Total Cost')]")
def _query_tool_explain_analyze(self):
query = """-- Explain analyze query
SELECT generate_series(1, 1000) as id order by id desc"""
wait = WebDriverWait(self.page.driver, 10)
self.page.fill_codemirror_area_with(query)
self.page.find_by_id("btn-query-dropdown").click()
self.page.find_by_id("btn-explain-analyze").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Data Output')
canvas = wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, "#datagrid .slick-viewport .grid-canvas"))
)
# Search for Actual Rows word in result
canvas.find_element_by_xpath("//*[contains(string(),'Actual Rows')]")
def _query_tool_explain_analyze_buffers(self):
query = """-- Explain analyze query with buffers
SELECT generate_series(1, 1000) as id order by id desc"""
wait = WebDriverWait(self.page.driver, 10)
self.page.fill_codemirror_area_with(query)
query_op = self.page.find_by_id("btn-query-dropdown")
query_op.click()
ActionChains(self.driver).move_to_element(
query_op.find_element_by_xpath(
"//li[contains(.,'Explain Options')]")).perform()
self.page.find_by_id("btn-explain-buffers").click()
self.page.find_by_id("btn-explain-analyze").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Data Output')
canvas = wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, "#datagrid .slick-viewport .grid-canvas"))
)
# Search for 'Shared Read Blocks' word in result
canvas.find_element_by_xpath("//*[contains(string(), 'Shared Read Blocks')]")
def _query_tool_explain_analyze_timing(self):
query = """-- Explain analyze query with timing
SELECT generate_series(1, 1000) as id order by id desc"""
wait = WebDriverWait(self.page.driver, 10)
self.page.fill_codemirror_area_with(query)
query_op = self.page.find_by_id("btn-query-dropdown")
query_op.click()
ActionChains(self.driver).move_to_element(
query_op.find_element_by_xpath(
"//li[contains(.,'Explain Options')]")).perform()
self.page.find_by_id("btn-explain-timing").click()
self.page.find_by_id("btn-explain-analyze").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Data Output')
canvas = wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, "#datagrid .slick-viewport .grid-canvas"))
)
# Search for 'Actual Total Time' word in result
canvas.find_element_by_xpath("//*[contains(string(), 'Actual Total Time')]")
def _query_tool_auto_commit_disabled(self):
table_name = 'query_tool_auto_commit_disabled_table'
query = """-- 1. Disable auto commit.
-- 2. Create table in public schema.
-- 3. ROLLBACK transaction.
-- 4. Check if table is *NOT* created.
CREATE TABLE public.{}();""".format(table_name)
wait = WebDriverWait(self.page.driver, 10)
self.page.fill_codemirror_area_with(query)
self.page.find_by_id("btn-query-dropdown").click()
auto_commit_btn = self.page.find_by_id("btn-auto-commit")
auto_commit_check = auto_commit_btn.find_element_by_tag_name("i")
# if auto commit is enabled then 'i' element will
# have 'auto-commit fa fa-check' classes
# if auto commit is disabled then 'i' element will
# have 'auto-commit fa fa-check visibility-hidden' classes
if 'auto-commit fa fa-check' == str(auto_commit_check.get_attribute(
'class')):
auto_commit_btn.click()
self.page.find_by_id("btn-flash").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Messages')
self.driver.find_element_by_xpath(
'//div[contains(@class, "sql-editor-message") and contains(string(), "CREATE TABLE")]'
)
self._clear_query_tool()
query = """-- 1. (Done) Disable auto commit.
-- 2. (Done) Create table in public schema.
-- 3. ROLLBACK transaction.
-- 4. Check if table is *NOT* created.
ROLLBACK;"""
self.page.fill_codemirror_area_with(query)
self.page.find_by_id("btn-flash").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Messages')
self.driver.find_element_by_xpath(
'//div[contains(@class, "sql-editor-message") and contains(string(), "ROLLBACK")]'
)
self._clear_query_tool()
query = """-- 1. (Done) Disable auto commit.
-- 2. (Done) Create table in public schema.
-- 3. (Done) ROLLBACK transaction.
-- 4. Check if table is *NOT* created.
SELECT relname FROM pg_class WHERE relkind IN ('r','s','t') and relnamespace = 2200::oid;"""
self.page.fill_codemirror_area_with(query)
self.page.find_by_id("btn-flash").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Data Output')
canvas = wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, "#datagrid .slick-viewport .grid-canvas")))
el = canvas.find_elements_by_xpath("//div[contains(@class, 'slick-cell') and contains(text(), '{}')]".format(table_name))
assert len(el) == 0, "Table '{}' created with auto commit disabled and without any explicit commit.".format(table_name)
def _query_tool_auto_commit_enabled(self):
table_name = 'query_tool_auto_commit_enabled_table'
query = """-- 1. END any open transaction.
-- 2. Enable auto commit.
-- 3. Create table in public schema.
-- 4. ROLLBACK transaction
-- 5. Check if table is created event after ROLLBACK.
END;
CREATE TABLE public.{}();""".format(table_name)
wait = WebDriverWait(self.page.driver, 10)
self.page.fill_codemirror_area_with(query)
self.page.find_by_id("btn-query-dropdown").click()
auto_commit_btn = self.page.find_by_id("btn-auto-commit")
auto_commit_check = auto_commit_btn.find_element_by_tag_name("i")
# if auto commit is enabled then 'i' element will
# have 'auto-commit fa fa-check' classes
# if auto commit is disabled then 'i' element will
# have 'auto-commit fa fa-check visibility-hidden' classes
if 'auto-commit fa fa-check visibility-hidden' == str(auto_commit_check.get_attribute(
'class')):
auto_commit_btn.click()
self.page.find_by_id("btn-flash").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Messages')
self.driver.find_element_by_xpath(
'//div[contains(@class, "sql-editor-message") and contains(string(), "CREATE TABLE")]'
)
self._clear_query_tool()
query = """-- 1. (Done) END any open transaction if any.
-- 2. (Done) Enable auto commit.
-- 3. (Done) Create table in public schema.
-- 4. ROLLBACK transaction
-- 5. Check if table is created event after ROLLBACK.
ROLLBACK;"""
self.page.fill_codemirror_area_with(query)
self.page.find_by_id("btn-flash").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Messages')
self.driver.find_element_by_xpath(
'//div[contains(@class, "sql-editor-message") and contains(string(), "ROLLBACK")]'
)
self._clear_query_tool()
query = """-- 1. (Done) END any open transaction if any.
-- 2. (Done) Enable auto commit.
-- 3. (Done) Create table in public schema.
-- 4. (Done) ROLLBACK transaction
-- 5. Check if table is created event after ROLLBACK.
SELECT relname FROM pg_class WHERE relkind IN ('r','s','t') and relnamespace = 2200::oid;"""
self.page.fill_codemirror_area_with(query)
self.page.find_by_id("btn-flash").click()
self.page.click_tab('Data Output')
self.page.wait_for_query_tool_loading_indicator_to_disappear()
canvas = wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, "#datagrid .slick-viewport .grid-canvas")))
el = canvas.find_elements_by_xpath("//div[contains(@class, 'slick-cell') and contains(text(), '{}')]".format(table_name))
assert len(el) != 0, "Table '{}' is not created with auto commit enabled.".format(table_name)
def _query_tool_auto_rollback_enabled(self):
table_name = 'query_tool_auto_rollback_enabled_table'
query = """-- 1. END any open transaction.
-- 2. Enable auto rollback and disable auto commit.
-- 3. Create table in public schema.
-- 4. Generate error in transaction.
-- 5. END transaction.
-- 6. Check if table is *NOT* created after ending transaction.
END;"""
self.page.fill_codemirror_area_with(query)
self.page.find_by_id("btn-flash").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self._clear_query_tool()
query = """-- 1. (Done) END any open transaction.
-- 2. Enable auto rollback and disable auto commit.
-- 3. Create table in public schema.
-- 4. Generate error in transaction.
-- 5. END transaction.
-- 6. Check if table is *NOT* created after ending transaction.
CREATE TABLE public.{}();""".format(table_name)
wait = WebDriverWait(self.page.driver, 10)
self.page.fill_codemirror_area_with(query)
self.page.find_by_id("btn-query-dropdown").click()
auto_rollback_btn = self.page.find_by_id("btn-auto-rollback")
auto_rollback_check = auto_rollback_btn.find_element_by_tag_name("i")
# if auto rollback is enabled then 'i' element will
# have 'auto-rollback fa fa-check' classes
# if auto rollback is disabled then 'i' element will
# have 'auto-rollback fa fa-check visibility-hidden' classes
if 'auto-rollback fa fa-check visibility-hidden' == str(auto_rollback_check.get_attribute(
'class')):
auto_rollback_btn.click()
auto_commit_btn = self.page.find_by_id("btn-auto-commit")
auto_commit_check = auto_commit_btn.find_element_by_tag_name("i")
# if auto commit is enabled then 'i' element will
# have 'auto-commit fa fa-check' classes
# if auto commit is disabled then 'i' element will
# have 'auto-commit fa fa-check visibility-hidden' classes
if 'auto-commit fa fa-check' == str(auto_commit_check.get_attribute(
'class')):
auto_commit_btn.click()
self.page.find_by_id("btn-flash").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Messages')
self.driver.find_element_by_xpath(
'//div[contains(@class, "sql-editor-message") and contains(string(), "CREATE TABLE")]'
)
self._clear_query_tool()
query = """-- 1. (Done) END any open transaction.
-- 2. (Done) Enable auto rollback and disable auto commit.
-- 3. (Done) Create table in public schema.
-- 4. Generate error in transaction.
-- 5. END transaction.
-- 6. Check if table is *NOT* created after ending transaction.
SELECT 1/0;"""
self.page.fill_codemirror_area_with(query)
self.page.find_by_id("btn-flash").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Messages')
self.driver.find_element_by_xpath(
'//div[contains(@class, "sql-editor-message") and contains(string(), "division by zero")]'
)
self._clear_query_tool()
query = """-- 1. (Done) END any open transaction.
-- 2. (Done) Enable auto rollback and disable auto commit.
-- 3. (Done) Create table in public schema.
-- 4. (Done) Generate error in transaction.
-- 5. END transaction.
-- 6. Check if table is *NOT* created after ending transaction.
END;"""
self.page.fill_codemirror_area_with(query)
self.page.find_by_id("btn-flash").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Messages')
self.driver.find_element_by_xpath(
'//div[contains(@class, "sql-editor-message") and contains(string(), "Query returned successfully")]'
)
self._clear_query_tool()
query = """-- 1. (Done) END any open transaction.
-- 2. (Done) Enable auto rollback and disable auto commit.
-- 3. (Done) Create table in public schema.
-- 4. (Done) Generate error in transaction.
-- 5. (Done) END transaction.
-- 6. Check if table is *NOT* created after ending transaction.
SELECT relname FROM pg_class WHERE relkind IN ('r','s','t') and relnamespace = 2200::oid;"""
self.page.fill_codemirror_area_with(query)
self.page.find_by_id("btn-flash").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Data Output')
canvas = wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, "#datagrid .slick-viewport .grid-canvas")))
el = canvas.find_elements_by_xpath("//div[contains(@class, 'slick-cell') and contains(text(), '{}')]".format(table_name))
assert len(el) == 0, "Table '{}' created even after ROLLBACK due to sql error.".format(table_name)
def _query_tool_cancel_query(self):
query = """-- 1. END any open transaction.
-- 2. Enable auto commit and Disable auto rollback.
-- 3. Execute long running query.
-- 4. Cancel long running query execution.
END;
SELECT 1, pg_sleep(10)"""
self.page.fill_codemirror_area_with(query)
self.page.find_by_id("btn-query-dropdown").click()
auto_rollback_btn = self.page.find_by_id("btn-auto-rollback")
auto_rollback_check = auto_rollback_btn.find_element_by_tag_name("i")
# if auto rollback is enabled then 'i' element will
# have 'auto-rollback fa fa-check' classes
# if auto rollback is disabled then 'i' element will
# have 'auto-rollback fa fa-check visibility-hidden' classes
if 'auto-rollback fa fa-check' == str(auto_rollback_check.get_attribute(
'class')):
auto_rollback_btn.click()
auto_commit_btn = self.page.find_by_id("btn-auto-commit")
auto_commit_check = auto_commit_btn.find_element_by_tag_name("i")
# if auto commit is enabled then 'i' element will
# have 'auto-commit fa fa-check' classes
# if auto commit is disabled then 'i' element will
# have 'auto-commit fa fa-check visibility-hidden' classes
if 'auto-commit fa fa-check visibility-hidden' == str(auto_commit_check.get_attribute(
'class')):
auto_commit_btn.click()
self.page.find_by_id("btn-flash").click()
self.driver.find_element_by_xpath("//*[@id='fetching_data']")
self.page.find_by_id("btn-cancel-query").click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Messages')
self.driver.find_element_by_xpath(
'//div[contains(@class, "sql-editor-message") and contains(string(), "canceling statement due to user request")]'
)

View File

@ -9,6 +9,7 @@
import json
import os
import time
from selenium.webdriver import ActionChains
from regression.python_test_utils import test_utils
from regression.feature_utils.base_feature_test import BaseFeatureTest
@ -205,6 +206,10 @@ CREATE TABLE public.defaults
self.page.driver.find_element_by_link_text("View Data")) \
.perform()
self.page.find_by_partial_link_text("View All Rows").click()
# wait until datagrid frame is loaded.
self.page.click_tab('Edit Data -')
self.wait.until(
EC.visibility_of_element_located(
(By.CSS_SELECTOR, 'iframe')
@ -242,6 +247,11 @@ CREATE TABLE public.defaults
self._update_cell(row1_cell2_xpath, ["1", "", "int"])
self.page.find_by_id("btn-save").click() # Save data
# There should be some delay after save button is clicked, as it
# takes some time to complete save ajax call otherwise discard unsaved
# changes dialog will appear if we try to execute query before previous
# save ajax is completed.
time.sleep(2)
# Verify row 1 and row 2 data
self._verify_row_data(False)
@ -254,6 +264,11 @@ CREATE TABLE public.defaults
self._update_cell(cell_xpath, config_data[str(idx)])
self.page.find_by_id("btn-save").click() # Save data
# There should be some delay after save button is clicked, as it
# takes some time to complete save ajax call otherwise discard unsaved
# changes dialog will appear if we try to execute query before previous
# save ajax is completed.
time.sleep(2)
def _verify_row_data(self, is_new_row):
self.page.find_by_id("btn-flash").click()
@ -264,17 +279,17 @@ CREATE TABLE public.defaults
xpath = "//*[contains(@class, 'ui-widget-content') and " \
"contains(@style, 'top:" + str(row_height) + "px')]"
# wait for stale element reference exception
self.page.wait_for_element_to_stale(xpath)
self.page.wait_for_query_tool_loading_indicator_to_disappear()
result_row = self.page.find_by_xpath(xpath)
# List of row values in an array
cells = [el.text for el in result_row.find_elements_by_tag_name('div')]
for idx in range(1, len(config_data.keys())):
# # after copy & paste row, the first cell of row 1 and
# # row 2(being primary keys) won't match
# # see if cell values matched to actual value
# after copy & paste row, the first cell of row 1 and
# row 2(being primary keys) won't match
# see if cell values matched to actual value
if idx != 1 and not is_new_row:
self.assertEquals(cells[idx], config_data[str(idx)][1])
elif is_new_row:

View File

@ -10,6 +10,9 @@
from selenium.webdriver import ActionChains
from regression.python_test_utils import test_utils
from regression.feature_utils.base_feature_test import BaseFeatureTest
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
import time
class CheckForXssFeatureTest(BaseFeatureTest):
@ -72,6 +75,7 @@ class CheckForXssFeatureTest(BaseFeatureTest):
def _connects_to_server(self):
self.page.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click()
time.sleep(2)
self.page.driver.find_element_by_link_text("Object").click()
ActionChains(self.page.driver) \
.move_to_element(self.page.driver.find_element_by_link_text("Create")) \
@ -152,11 +156,16 @@ class CheckForXssFeatureTest(BaseFeatureTest):
self.page.fill_codemirror_area_with("select '<img src=\"x\" onerror=\"console.log(1)\">'")
time.sleep(1)
self.page.find_by_id("btn-flash").click()
time.sleep(2)
wait = WebDriverWait(self.page.driver, 5)
source_code = self.page.find_by_xpath(
"//*[@id='0']//*[@id='datagrid']/div[5]/div/div[1]/div[2]"
).get_attribute('innerHTML')
result_row = self.page.find_by_xpath(
"//*[contains(@class, 'ui-widget-content') and contains(@style, 'top:0px')]"
)
cells = result_row.find_elements_by_tag_name('div')
# remove first element as it is row number.
source_code = cells[1].get_attribute('innerHTML')
self._check_escaped_characters(
source_code,

View File

@ -42,6 +42,7 @@ class CheckDebuggerForXssFeatureTest(BaseFeatureTest):
def _connects_to_server(self):
self.page.find_by_xpath("//*[@class='aciTreeText' and .='Servers']").click()
time.sleep(2)
self.page.driver.find_element_by_link_text("Object").click()
ActionChains(self.page.driver) \
.move_to_element(self.page.driver.find_element_by_link_text("Create")) \

View File

@ -3,6 +3,7 @@ import 'slickgrid/slick-default-theme.css';
import 'slickgrid/css/smoothness/jquery-ui-1.11.3.custom.css';
import 'slickgrid/slick.core';
import 'slickgrid/slick.grid';
import 'slickgrid/slick.dataview';
import 'slickgrid/slick.editors';
import 'slickgrid/slick.formatters';
import 'slickgrid/plugins/slick.autotooltips';

View File

@ -4,13 +4,18 @@ define([
'slickgrid',
], function ($, RangeSelectionHelper) {
var ColumnSelector = function () {
var Slick = window.Slick;
var gridEventBus = new Slick.EventHandler();
var Slick = window.Slick,
gridEventBus = new Slick.EventHandler(),
onBeforeColumnSelectAll = new Slick.Event(),
onColumnSelectAll = new Slick.Event();
var init = function (grid) {
gridEventBus.subscribe(grid.onHeaderClick, handleHeaderClick.bind(null, grid));
grid.getSelectionModel().onSelectedRangesChanged
.subscribe(handleSelectedRangesChanged.bind(null, grid));
onColumnSelectAll.subscribe(function(e, args) {
updateRanges(args.grid, args.column.id);
});
};
var handleHeaderClick = function (grid, event, args) {
@ -21,11 +26,20 @@ define([
if (isColumnSelectable(columnDefinition)) {
var $columnHeader = $(event.target);
if (hasClickedChildOfColumnHeader(event)) {
if ($(event.target).hasClass('slick-resizable-handle')) {
return;
}
$columnHeader = $(event.target).parents('.slick-header-column');
}
$columnHeader.toggleClass('selected');
updateRanges(grid, columnDefinition.id);
if ($columnHeader.hasClass('selected')) {
onBeforeColumnSelectAll.notify(args, event);
}
if (!(event.isPropagationStopped() || event.isImmediatePropagationStopped())) {
updateRanges(grid, columnDefinition.id);
}
}
};
@ -107,6 +121,8 @@ define([
$.extend(this, {
'init': init,
'getColumnDefinitions': getColumnDefinitions,
'onBeforeColumnSelectAll': onBeforeColumnSelectAll,
'onColumnSelectAll': onColumnSelectAll,
});
};
return ColumnSelector;

View File

@ -12,19 +12,19 @@ function ($, _, clipboard, RangeSelectionHelper, rangeBoundaryNavigator) {
var grid = self.slickgrid;
var columnDefinitions = grid.getColumns();
var selectedRanges = grid.getSelectionModel().getSelectedRanges();
var data = grid.getData();
var dataView = grid.getData();
var rows = grid.getSelectedRows();
if (RangeSelectionHelper.areAllRangesCompleteRows(grid, selectedRanges)) {
self.copied_rows = rows.map(function (rowIndex) {
return data[rowIndex];
return grid.getDataItem(rowIndex);
});
setPasteRowButtonEnablement(self.can_edit, true);
} else {
self.copied_rows = [];
setPasteRowButtonEnablement(self.can_edit, false);
}
var csvText = rangeBoundaryNavigator.rangesToCsv(data, columnDefinitions, selectedRanges);
var csvText = rangeBoundaryNavigator.rangesToCsv(dataView.getItems(), columnDefinitions, selectedRanges);
if (csvText) {
clipboard.copyTextToClipboard(csvText);
}

View File

@ -6,21 +6,31 @@ define(['jquery',
'sources/url_for',
], function ($, gettext, ColumnSelector, RowSelector, RangeSelectionHelper, url_for) {
var GridSelector = function (columnDefinitions) {
var rowSelector = new RowSelector(columnDefinitions);
var columnSelector = new ColumnSelector(columnDefinitions);
var Slick = window.Slick,
rowSelector = new RowSelector(columnDefinitions),
columnSelector = new ColumnSelector(columnDefinitions),
onBeforeGridSelectAll = new Slick.Event(),
onGridSelectAll = new Slick.Event(),
onBeforeGridColumnSelectAll = columnSelector.onBeforeColumnSelectAll,
onGridColumnSelectAll = columnSelector.onColumnSelectAll;
var init = function (grid) {
this.grid = grid;
grid.onHeaderClick.subscribe(function (event, eventArguments) {
if (eventArguments.column.selectAllOnClick) {
toggleSelectAll(grid);
if (eventArguments.column.selectAllOnClick && !$(event.target).hasClass('slick-resizable-handle')) {
toggleSelectAll(grid, event, eventArguments);
}
});
grid.getSelectionModel().onSelectedRangesChanged
.subscribe(handleSelectedRangesChanged.bind(null, grid));
.subscribe(handleSelectedRangesChanged.bind(null, grid));
grid.registerPlugin(rowSelector);
grid.registerPlugin(columnSelector);
onGridSelectAll.subscribe(function(e, args) {
RangeSelectionHelper.selectAll(args.grid);
});
};
var getColumnDefinitions = function (columnDefinitions) {
@ -45,11 +55,14 @@ define(['jquery',
}
}
function toggleSelectAll(grid) {
function toggleSelectAll(grid, event, eventArguments) {
if (RangeSelectionHelper.isEntireGridSelected(grid)) {
selectNone(grid);
} else {
RangeSelectionHelper.selectAll(grid);
onBeforeGridSelectAll.notify(eventArguments, event);
if (!(event.isPropagationStopped() || event.isImmediatePropagationStopped())) {
RangeSelectionHelper.selectAll(grid);
}
}
}
@ -61,6 +74,10 @@ define(['jquery',
$.extend(this, {
'init': init,
'getColumnDefinitions': getColumnDefinitions,
'onBeforeGridSelectAll': onBeforeGridSelectAll,
'onGridSelectAll': onGridSelectAll,
'onBeforeGridColumnSelectAll': onBeforeGridColumnSelectAll,
'onGridColumnSelectAll': onGridColumnSelectAll,
});
};

View File

@ -58,6 +58,7 @@ function (RangeSelectionHelper) {
},
rangesToCsv: function (data, columnDefinitions, selectedRanges) {
var rowRangeBounds = selectedRanges.map(function (range) {
return [range.fromRow, range.toRow];
});
@ -72,6 +73,7 @@ function (RangeSelectionHelper) {
var csvRows = this.mapOver2DArray(rowRangeBounds, colRangeBounds, this.csvCell.bind(this, data, columnDefinitions), function (rowData) {
return rowData.join(',');
});
return csvRows.join('\n');
},
@ -101,7 +103,7 @@ function (RangeSelectionHelper) {
},
csvCell: function (data, columnDefinitions, rowId, colId) {
var val = data[rowId][columnDefinitions[colId].pos];
var val = data[rowId][columnDefinitions[colId].field];
if (val && _.isObject(val)) {
val = '\'' + JSON.stringify(val) + '\'';

View File

@ -82,7 +82,8 @@ define([
formatter: function (rowIndex) {
return '<span ' +
'data-row="' + rowIndex + '" ' +
'data-cell-type="row-header-selector"/>';
'data-cell-type="row-header-selector">' +
(rowIndex+1) + '</span>';
},
width: 30,
});

View File

@ -22,53 +22,44 @@ define(
$(selector).prop('disabled', false);
}
function getRowPrimaryKeyValuesToStage(selectedRows, primaryKeyColumnIndices, gridData) {
function getRowPrimaryKeyValuesToStage(selectedRows, primaryKeys, dataView, client_primary_key) {
return _.reduce(selectedRows, function (primaryKeyValuesToStage, dataGridRowIndex) {
var gridRow = gridData[dataGridRowIndex];
if (isRowMissingPrimaryKeys(gridRow, primaryKeyColumnIndices)) {
var gridRow = dataView.getItem(dataGridRowIndex);
if (isRowMissingPrimaryKeys(gridRow, primaryKeys)) {
return primaryKeyValuesToStage;
}
var tempPK = gridRow.__temp_PK;
primaryKeyValuesToStage[tempPK] = getSingleRowPrimaryKeyValueToStage(primaryKeyColumnIndices, gridRow);
var tempPK = gridRow[client_primary_key];
primaryKeyValuesToStage[tempPK] = getSingleRowPrimaryKeyValueToStage(primaryKeys, gridRow);
return primaryKeyValuesToStage;
}, {});
}
function isRowMissingPrimaryKeys(gridRow, primaryKeyColumnIndices) {
function isRowMissingPrimaryKeys(gridRow, primaryKeys) {
if (_.isUndefined(gridRow)) {
return true;
}
return !_.isUndefined(
_.find(primaryKeyColumnIndices, function (pkIndex) {
return _.isUndefined(gridRow[pkIndex]);
_.find(primaryKeys , function (pk) {
return _.isUndefined(gridRow[pk]);
})
);
}
function getSingleRowPrimaryKeyValueToStage(primaryKeyColumnIndices, gridRow) {
function getSingleRowPrimaryKeyValueToStage(primaryKeys, gridRow) {
var rowToStage = {};
if (primaryKeyColumnIndices.length) {
_.each(_.keys(gridRow), function (columnPos) {
if (_.contains(primaryKeyColumnIndices, Number(columnPos)))
rowToStage[columnPos] = gridRow[columnPos];
if (primaryKeys && primaryKeys.length) {
_.each(_.keys(gridRow), function (columnNames) {
if (_.contains(primaryKeys, columnNames))
rowToStage[columnNames] = gridRow[columnNames];
});
}
return rowToStage;
}
function getPrimaryKeysForSelectedRows(self, selectedRows) {
var primaryKeyColumnIndices = _.map(_.keys(self.keys), function (columnName) {
var columnInfo = _.findWhere(self.columns, {name: columnName});
return columnInfo['pos'];
});
var gridData = self.grid.getData();
var stagedRows = getRowPrimaryKeyValuesToStage(selectedRows, primaryKeyColumnIndices, gridData);
var dataView = self.grid.getData();
var stagedRows = getRowPrimaryKeyValuesToStage(selectedRows, _.keys(self.keys), dataView, self.client_primary_key);
return stagedRows;
}
@ -114,4 +105,4 @@ define(
};
return setStagedRows;
}
);
);

View File

@ -76,18 +76,18 @@
last_value = (column_type === 'number') ?
(_.isEmpty(last_value) || last_value) : last_value;
item[args.column.pos] = state;
item[args.column.field] = state;
if (last_value && _.isNull(state) &&
(_.isUndefined(grid.copied_rows[row]) ||
_.isUndefined(grid.copied_rows[row][cell]))
) {
item[args.column.pos] = undefined;
item[args.column.field] = undefined;
if (grid.copied_rows[row] == undefined) grid.copied_rows[row] = [];
grid.copied_rows[row][cell] = 1;
}
}
else {
item[args.column.pos] = state;
item[args.column.field] = state;
}
}
@ -189,14 +189,14 @@
this.loadValue = function (item) {
var col = args.column;
if (_.isUndefined(item[args.column.pos]) && col.has_default_val) {
if (_.isUndefined(item[args.column.field]) && col.has_default_val) {
$input.val(defaultValue = "");
}
else if (item[args.column.pos] === "") {
else if (item[args.column.field] === "") {
$input.val(defaultValue = "''");
}
else {
$input.val(defaultValue = item[args.column.pos]);
$input.val(defaultValue = item[args.column.field]);
$input.select();
}
};
@ -323,7 +323,7 @@
};
this.loadValue = function (item) {
var data = defaultValue = item[args.column.pos];
var data = defaultValue = item[args.column.field];
if (data && typeof data === "object" && !Array.isArray(data)) {
data = JSON.stringify(data);
} else if (Array.isArray(data)) {
@ -443,7 +443,7 @@
};
this.loadValue = function (item) {
$input.val(defaultValue = item[args.column.pos]);
$input.val(defaultValue = item[args.column.field]);
$input.select();
};
@ -452,7 +452,7 @@
};
this.applyValue = function (item, state) {
item[args.column.pos] = state;
item[args.column.field] = state;
};
this.isValueChanged = function () {
@ -531,13 +531,13 @@
};
this.loadValue = function (item) {
defaultValue = item[args.column.pos];
if (_.isNull(defaultValue)|| _.isUndefined(defaultValue)) {
defaultValue = item[args.column.field];
if (_.isNull(defaultValue)||_.isUndefined(defaultValue)) {
$select.prop('indeterminate', true);
$select.data('checked', 2);
}
else {
defaultValue = !!item[args.column.pos];
defaultValue = !!item[args.column.field];
if (defaultValue) {
$select.prop('checked', true);
$select.data('checked', 0);
@ -556,7 +556,7 @@
};
this.applyValue = function (item, state) {
item[args.column.pos] = state;
item[args.column.field] = state;
};
this.isValueChanged = function () {
@ -648,7 +648,7 @@
};
this.loadValue = function (item) {
var data = defaultValue = item[args.column.pos];
var data = defaultValue = item[args.column.field];
if (typeof data === "object" && !Array.isArray(data)) {
data = JSON.stringify(data);
} else if (Array.isArray(data)) {
@ -671,7 +671,7 @@
};
this.applyValue = function (item, state) {
item[args.column.pos] = state;
item[args.column.field] = state;
};
this.isValueChanged = function () {
@ -725,7 +725,7 @@
};
this.loadValue = function (item) {
var value = item[args.column.pos];
var value = item[args.column.field];
// Check if value is null or undefined
if (value === undefined && typeof value === "undefined") {
@ -858,7 +858,7 @@
};
this.loadValue = function (item) {
defaultValue = item[args.column.pos];
defaultValue = item[args.column.field];
$input.val(defaultValue);
$input[0].defaultValue = defaultValue;
$input.select();

View File

@ -27,7 +27,7 @@ from pgadmin.utils.sqlautocomplete.autocomplete import SQLAutoComplete
from pgadmin.misc.file_manager import Filemanager
from config import PG_DEFAULT_DRIVER
from config import PG_DEFAULT_DRIVER, ON_DEMAND_RECORD_COUNT
MODULE_NAME = 'sqleditor'
@ -82,9 +82,9 @@ class SqlEditorModule(PgAdminModule):
'sqleditor.view_data_start',
'sqleditor.query_tool_start',
'sqleditor.query_tool_preferences',
'sqleditor.get_columns',
'sqleditor.poll',
'sqleditor.fetch_types',
'sqleditor.fetch',
'sqleditor.fetch_all',
'sqleditor.save',
'sqleditor.get_filter',
'sqleditor.apply_filter',
@ -261,13 +261,32 @@ def start_view_data(trans_id):
# Check the transaction and connection status
status, error_msg, conn, trans_obj, session_obj = check_transaction_status(trans_id)
# get the default connection as current connection which is attached to
# trans id holds the cursor which has query result so we cannot use that
# connection to execute another query otherwise we'll lose query result.
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(trans_obj.sid)
default_conn = manager.connection(did=trans_obj.did)
# Connect to the Server if not connected.
if not default_conn.connected():
status, msg = default_conn.connect()
if not status:
return make_json_response(
data={'status': status, 'result': u"{}".format(msg)}
)
if status and conn is not None \
and trans_obj is not None and session_obj is not None:
try:
# set fetched row count to 0 as we are executing query again.
trans_obj.update_fetched_row_cnt(0)
session_obj['command_obj'] = pickle.dumps(trans_obj, -1)
# Fetch the sql and primary_keys from the object
sql = trans_obj.get_sql()
pk_names, primary_keys = trans_obj.get_primary_keys()
pk_names, primary_keys = trans_obj.get_primary_keys(default_conn)
# Fetch the applied filter.
filter_applied = trans_obj.is_filter_applied()
@ -338,6 +357,8 @@ def start_query_tool(trans_id):
# Use pickle.loads function to get the command object
session_obj = grid_data[str(trans_id)]
trans_obj = pickle.loads(session_obj['command_obj'])
# set fetched row count to 0 as we are executing query again.
trans_obj.update_fetched_row_cnt(0)
can_edit = False
can_filter = False
@ -467,66 +488,6 @@ def preferences(trans_id):
return success_return()
@blueprint.route(
'/columns/<int:trans_id>', methods=["GET"], endpoint='get_columns'
)
@login_required
def get_columns(trans_id):
"""
This method will returns list of columns of last async query.
Args:
trans_id: unique transaction id
"""
columns = dict()
columns_info = None
primary_keys = None
rset = None
status, error_msg, conn, trans_obj, session_obj = check_transaction_status(trans_id)
if status and conn is not None and session_obj is not None:
ver = conn.manager.version
# Get the template path for the column
template_path = 'column/sql/#{0}#'.format(ver)
command_obj = pickle.loads(session_obj['command_obj'])
if hasattr(command_obj, 'obj_id'):
SQL = render_template("/".join([template_path,
'nodes.sql']),
tid=command_obj.obj_id)
# rows with attribute not_null
status, rset = conn.execute_2darray(SQL)
if not status:
return internal_server_error(errormsg=rset)
# Check PK column info is available or not
if 'primary_keys' in session_obj:
primary_keys = session_obj['primary_keys']
# Fetch column information
columns_info = conn.get_column_info()
if columns_info is not None:
for key, col in enumerate(columns_info):
col_type = dict()
col_type['type_code'] = col['type_code']
col_type['type_name'] = None
if rset:
col_type['not_null'] = col['not_null'] = \
rset['rows'][key]['not_null']
col_type['has_default_val'] = col['has_default_val'] = \
rset['rows'][key]['has_default_val']
columns[col['name']] = col_type
# As we changed the transaction object we need to
# restore it and update the session variable.
session_obj['columns_info'] = columns
update_session_grid_transaction(trans_id, session_obj)
return make_json_response(data={'status': True,
'columns': columns_info,
'primary_keys': primary_keys})
@blueprint.route('/poll/<int:trans_id>', methods=["GET"], endpoint='poll')
@login_required
@ -539,12 +500,21 @@ def poll(trans_id):
"""
result = None
rows_affected = 0
rows_fetched_from = 0
rows_fetched_to = 0
has_more_rows = False
additional_result = []
columns = dict()
columns_info = None
primary_keys = None
types = {}
client_primary_key = None
rset = None
# Check the transaction and connection status
status, error_msg, conn, trans_obj, session_obj = check_transaction_status(trans_id)
if status and conn is not None and session_obj is not None:
status, result = conn.poll(formatted_exception_msg=True)
status, result = conn.poll(formatted_exception_msg=True, no_result=True)
if not status:
return internal_server_error(result)
elif status == ASYNC_OK:
@ -559,6 +529,80 @@ def poll(trans_id):
if (trans_status == TX_STATUS_INERROR and
trans_obj.auto_rollback):
conn.execute_void("ROLLBACK;")
st, result = conn.async_fetchmany_2darray(ON_DEMAND_RECORD_COUNT)
if st:
if 'primary_keys' in session_obj:
primary_keys = session_obj['primary_keys']
# Fetch column information
columns_info = conn.get_column_info()
client_primary_key = generate_client_primary_key_name(
columns_info
)
session_obj['client_primary_key'] = client_primary_key
if columns_info is not None:
command_obj = pickle.loads(session_obj['command_obj'])
if hasattr(command_obj, 'obj_id'):
# Get the template path for the column
template_path = 'column/sql/#{0}#'.format(
conn.manager.version
)
SQL = render_template("/".join([template_path,
'nodes.sql']),
tid=command_obj.obj_id)
# rows with attribute not_null
colst, rset = conn.execute_2darray(SQL)
if not colst:
return internal_server_error(errormsg=rset)
for key, col in enumerate(columns_info):
col_type = dict()
col_type['type_code'] = col['type_code']
col_type['type_name'] = None
columns[col['name']] = col_type
if rset:
col_type['not_null'] = col['not_null'] = \
rset['rows'][key]['not_null']
col_type['has_default_val'] = \
col['has_default_val'] = \
rset['rows'][key]['has_default_val']
if columns:
st, types = fetch_pg_types(columns, trans_obj)
if not st:
return internal_server_error(types)
for col_info in columns.values():
for col_type in types:
if col_type['oid'] == col_info['type_code']:
col_info['type_name'] = col_type['typname']
session_obj['columns_info'] = columns
# status of async_fetchmany_2darray is True and result is none
# means nothing to fetch
if result and rows_affected > -1:
res_len = len(result)
if res_len == ON_DEMAND_RECORD_COUNT:
has_more_rows = True
if res_len > 0:
rows_fetched_from = trans_obj.get_fetched_row_cnt()
trans_obj.update_fetched_row_cnt(rows_fetched_from + res_len)
rows_fetched_from += 1
rows_fetched_to = trans_obj.get_fetched_row_cnt()
session_obj['command_obj'] = pickle.dumps(trans_obj, -1)
# As we changed the transaction object we need to
# restore it and update the session variable.
update_session_grid_transaction(trans_id, session_obj)
elif status == ASYNC_EXECUTION_ABORTED:
status = 'Cancel'
else:
@ -599,53 +643,123 @@ def poll(trans_id):
data={
'status': status, 'result': result,
'rows_affected': rows_affected,
'additional_messages': additional_messages
'rows_fetched_from': rows_fetched_from,
'rows_fetched_to': rows_fetched_to,
'additional_messages': additional_messages,
'has_more_rows': has_more_rows,
'colinfo': columns_info,
'primary_keys': primary_keys,
'types': types,
'client_primary_key': client_primary_key
}
)
@blueprint.route(
'/fetch/types/<int:trans_id>', methods=["GET"], endpoint='fetch_types'
)
@blueprint.route('/fetch/<int:trans_id>', methods=["GET"], endpoint='fetch')
@blueprint.route('/fetch/<int:trans_id>/<int:fetch_all>', methods=["GET"], endpoint='fetch_all')
@login_required
def fetch_pg_types(trans_id):
def fetch(trans_id, fetch_all=None):
result = None
has_more_rows = False
rows_fetched_from = 0
rows_fetched_to = 0
fetch_row_cnt = -1 if fetch_all == 1 else ON_DEMAND_RECORD_COUNT
# Check the transaction and connection status
status, error_msg, conn, trans_obj, session_obj = check_transaction_status(trans_id)
if status and conn is not None and session_obj is not None:
status, result = conn.async_fetchmany_2darray(fetch_row_cnt)
if not status:
status = 'Error'
else:
status = 'Success'
res_len = len(result)
if fetch_row_cnt != -1 and res_len == ON_DEMAND_RECORD_COUNT:
has_more_rows = True
if res_len:
rows_fetched_from = trans_obj.get_fetched_row_cnt()
trans_obj.update_fetched_row_cnt(rows_fetched_from + res_len)
rows_fetched_from += 1
rows_fetched_to = trans_obj.get_fetched_row_cnt()
session_obj['command_obj'] = pickle.dumps(trans_obj, -1)
update_session_grid_transaction(trans_id, session_obj)
else:
status = 'NotConnected'
result = error_msg
return make_json_response(
data={
'status': status, 'result': result,
'has_more_rows': has_more_rows,
'rows_fetched_from': rows_fetched_from,
'rows_fetched_to': rows_fetched_to
}
)
def fetch_pg_types(columns_info, trans_obj):
"""
This method is used to fetch the pg types, which is required
to map the data type comes as a result of the query.
Args:
trans_id: unique transaction id
columns_info:
"""
# Check the transaction and connection status
status, error_msg, conn, trans_obj, session_obj = check_transaction_status(trans_id)
if status and conn is not None \
and trans_obj is not None and session_obj is not None:
res = {}
if 'columns_info' in session_obj \
and session_obj['columns_info'] is not None:
# get the default connection as current connection attached to trans id
# holds the cursor which has query result so we cannot use that connection
# to execute another query otherwise we'll lose query result.
oids = [session_obj['columns_info'][col]['type_code'] for col in session_obj['columns_info']]
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(trans_obj.sid)
default_conn = manager.connection(did=trans_obj.did)
if oids:
status, res = conn.execute_dict(
u"""SELECT oid, format_type(oid,null) as typname FROM pg_type WHERE oid IN %s ORDER BY oid;
# Connect to the Server if not connected.
res = []
if not default_conn.connected():
status, msg = default_conn.connect()
if not status:
return status, msg
oids = [columns_info[col]['type_code'] for col in columns_info]
if oids:
status, res = default_conn.execute_dict(
u"""SELECT oid, format_type(oid,null) as typname FROM pg_type WHERE oid IN %s ORDER BY oid;
""", [tuple(oids)])
if status:
# iterate through pg_types and update the type name in session object
for record in res['rows']:
for col in session_obj['columns_info']:
type_obj = session_obj['columns_info'][col]
if type_obj['type_code'] == record['oid']:
type_obj['type_name'] = record['typname']
if not status:
return False, res
update_session_grid_transaction(trans_id, session_obj)
return status, res['rows']
else:
status = False
res = error_msg
return True, []
return make_json_response(data={'status': status, 'result': res})
def generate_client_primary_key_name(columns_info):
temp_key = '__temp_PK'
if not columns_info:
return temp_key
initial_temp_key_len = len(temp_key)
duplicate = False
suffix = 1
while 1:
for col in columns_info:
if col['name'] == temp_key:
duplicate = True
break
if duplicate:
if initial_temp_key_len == len(temp_key):
temp_key += str(suffix)
suffix += 1
else:
temp_key = temp_key[:-1] + str(suffix)
suffix += 1
duplicate = False
else:
break
return temp_key
@blueprint.route(
@ -659,7 +773,6 @@ def save(trans_id):
Args:
trans_id: unique transaction id
"""
if request.data:
changed_data = json.loads(request.data, encoding='utf-8')
else:
@ -669,7 +782,6 @@ def save(trans_id):
status, error_msg, conn, trans_obj, session_obj = check_transaction_status(trans_id)
if status and conn is not None \
and trans_obj is not None and session_obj is not None:
setattr(trans_obj, 'columns_info', session_obj['columns_info'])
# If there is no primary key found then return from the function.
if len(session_obj['primary_keys']) <= 0 or len(changed_data) <= 0:
@ -680,7 +792,22 @@ def save(trans_id):
}
)
status, res, query_res, _rowid = trans_obj.save(changed_data)
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(trans_obj.sid)
default_conn = manager.connection(did=trans_obj.did)
# Connect to the Server if not connected.
if not default_conn.connected():
status, msg = default_conn.connect()
if not status:
return make_json_response(
data={'status': status, 'result': u"{}".format(msg)}
)
status, res, query_res, _rowid = trans_obj.save(
changed_data,
session_obj['columns_info'],
session_obj['client_primary_key'],
default_conn)
else:
status = False
res = error_msg

View File

@ -258,7 +258,21 @@ class SQLFilter(object):
return status, result
class GridCommand(BaseCommand, SQLFilter):
class FetchedRowTracker(object):
"""
Keeps track of fetched row count.
"""
def __init__(self, **kwargs):
self.fetched_rows = 0
def get_fetched_row_cnt(self):
return self.fetched_rows
def update_fetched_row_cnt(self, rows_cnt):
self.fetched_rows = rows_cnt
class GridCommand(BaseCommand, SQLFilter, FetchedRowTracker):
"""
class GridCommand(object)
@ -290,6 +304,7 @@ class GridCommand(BaseCommand, SQLFilter):
"""
BaseCommand.__init__(self, **kwargs)
SQLFilter.__init__(self, **kwargs)
FetchedRowTracker.__init__(self, **kwargs)
# Save the connection id, command type
self.conn_id = kwargs['conn_id'] if 'conn_id' in kwargs else None
@ -299,10 +314,10 @@ class GridCommand(BaseCommand, SQLFilter):
if self.cmd_type == VIEW_FIRST_100_ROWS or self.cmd_type == VIEW_LAST_100_ROWS:
self.limit = 100
def get_primary_keys(self):
def get_primary_keys(self, *args, **kwargs):
return None, None
def save(self, changed_data):
def save(self, changed_data, default_conn=None):
return forbidden(errmsg=gettext("Data cannot be saved for the current object."))
def get_limit(self):
@ -340,14 +355,14 @@ class TableCommand(GridCommand):
# call base class init to fetch the table name
super(TableCommand, self).__init__(**kwargs)
def get_sql(self):
def get_sql(self, default_conn=None):
"""
This method is used to create a proper SQL query
to fetch the data for the specified table
"""
# Fetch the primary keys for the table
pk_names, primary_keys = self.get_primary_keys()
pk_names, primary_keys = self.get_primary_keys(default_conn)
sql_filter = self.get_filter()
@ -362,13 +377,16 @@ class TableCommand(GridCommand):
return sql
def get_primary_keys(self):
def get_primary_keys(self, default_conn=None):
"""
This function is used to fetch the primary key columns.
"""
driver = get_driver(PG_DEFAULT_DRIVER)
manager = driver.connection_manager(self.sid)
conn = manager.connection(did=self.did, conn_id=self.conn_id)
if default_conn is None:
manager = driver.connection_manager(self.sid)
conn = manager.connection(did=self.did, conn_id=self.conn_id)
else:
conn = default_conn
pk_names = ''
primary_keys = OrderedDict()
@ -400,7 +418,11 @@ class TableCommand(GridCommand):
def can_filter(self):
return True
def save(self, changed_data):
def save(self,
changed_data,
columns_info,
client_primary_key='__temp_PK',
default_conn=None):
"""
This function is used to save the data into the database.
Depending on condition it will either update or insert the
@ -408,10 +430,16 @@ class TableCommand(GridCommand):
Args:
changed_data: Contains data to be saved
columns_info:
default_conn:
client_primary_key:
"""
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(self.sid)
conn = manager.connection(did=self.did, conn_id=self.conn_id)
driver = get_driver(PG_DEFAULT_DRIVER)
if default_conn is None:
manager = driver.connection_manager(self.sid)
conn = manager.connection(did=self.did, conn_id=self.conn_id)
else:
conn = default_conn
status = False
res = None
@ -421,14 +449,6 @@ class TableCommand(GridCommand):
list_of_sql = []
_rowid = None
# Replace column positions with names
def set_column_names(data):
new_data = {}
for key in data:
new_data[changed_data['columns'][int(key)]['name']] = data[key]
return new_data
if conn.connected():
# Start the transaction
@ -443,6 +463,20 @@ class TableCommand(GridCommand):
if len(changed_data[of_type]) < 1:
continue
column_type = {}
for each_col in columns_info:
if (
columns_info[each_col]['not_null'] and
not columns_info[each_col][
'has_default_val']
):
column_data[each_col] = None
column_type[each_col] =\
columns_info[each_col]['type_name']
else:
column_type[each_col] = \
columns_info[each_col]['type_name']
# For newly added rows
if of_type == 'added':
@ -451,37 +485,18 @@ class TableCommand(GridCommand):
# no_default_value, set column to blank, instead
# of not null which is set by default.
column_data = {}
column_type = {}
pk_names, primary_keys = self.get_primary_keys()
for each_col in self.columns_info:
if (
self.columns_info[each_col]['not_null'] and
not self.columns_info[each_col][
'has_default_val']
):
column_data[each_col] = None
column_type[each_col] =\
self.columns_info[each_col]['type_name']
else:
column_type[each_col] = \
self.columns_info[each_col]['type_name']
for each_row in changed_data[of_type]:
data = changed_data[of_type][each_row]['data']
# Remove our unique tracking key
data.pop('__temp_PK', None)
data.pop(client_primary_key, None)
data.pop('is_row_copied', None)
data = set_column_names(data)
data_type = set_column_names(changed_data[of_type][each_row]['data_type'])
list_of_rowid.append(data.get('__temp_PK'))
list_of_rowid.append(data.get(client_primary_key))
# Update columns value and data type
# with columns having not_null=False and has
# no default value
# Update columns value with columns having
# not_null=False and has no default value
column_data.update(data)
column_type.update(data_type)
sql = render_template("/".join([self.sql_path, 'insert.sql']),
data_to_be_saved=column_data,
@ -497,15 +512,14 @@ class TableCommand(GridCommand):
# For updated rows
elif of_type == 'updated':
for each_row in changed_data[of_type]:
data = set_column_names(changed_data[of_type][each_row]['data'])
pk = set_column_names(changed_data[of_type][each_row]['primary_keys'])
data_type = set_column_names(changed_data[of_type][each_row]['data_type'])
data = changed_data[of_type][each_row]['data']
pk = changed_data[of_type][each_row]['primary_keys']
sql = render_template("/".join([self.sql_path, 'update.sql']),
data_to_be_saved=data,
primary_keys=pk,
object_name=self.object_name,
nsp_name=self.nsp_name,
data_type=data_type)
data_type=column_type)
list_of_sql.append(sql)
list_of_rowid.append(data)
@ -519,18 +533,19 @@ class TableCommand(GridCommand):
rows_to_delete.append(changed_data[of_type][each_row])
# Fetch the keys for SQL generation
if is_first:
# We need to covert dict_keys to normal list in Python3
# In Python2, it's already a list & We will also fetch column names using index
keys = [
changed_data['columns'][int(k)]['name']
for k in list(changed_data[of_type][each_row].keys())
]
# We need to covert dict_keys to normal list in
# Python3
# In Python2, it's already a list & We will also
# fetch column names using index
keys = list(changed_data[of_type][each_row].keys())
no_of_keys = len(keys)
is_first = False
# Map index with column name for each row
for row in rows_to_delete:
for k, v in row.items():
# Set primary key with label & delete index based mapped key
# Set primary key with label & delete index based
# mapped key
try:
row[changed_data['columns'][int(k)]['name']] = v
except ValueError:
@ -597,7 +612,7 @@ class ViewCommand(GridCommand):
# call base class init to fetch the table name
super(ViewCommand, self).__init__(**kwargs)
def get_sql(self):
def get_sql(self, default_conn=None):
"""
This method is used to create a proper SQL query
to fetch the data for the specified view
@ -652,7 +667,7 @@ class ForeignTableCommand(GridCommand):
# call base class init to fetch the table name
super(ForeignTableCommand, self).__init__(**kwargs)
def get_sql(self):
def get_sql(self, default_conn=None):
"""
This method is used to create a proper SQL query
to fetch the data for the specified foreign table
@ -697,7 +712,7 @@ class CatalogCommand(GridCommand):
# call base class init to fetch the table name
super(CatalogCommand, self).__init__(**kwargs)
def get_sql(self):
def get_sql(self, default_conn=None):
"""
This method is used to create a proper SQL query
to fetch the data for the specified catalog object
@ -722,7 +737,7 @@ class CatalogCommand(GridCommand):
return True
class QueryToolCommand(BaseCommand):
class QueryToolCommand(BaseCommand, FetchedRowTracker):
"""
class QueryToolCommand(BaseCommand)
@ -732,13 +747,15 @@ class QueryToolCommand(BaseCommand):
def __init__(self, **kwargs):
# call base class init to fetch the table name
super(QueryToolCommand, self).__init__(**kwargs)
BaseCommand.__init__(self, **kwargs)
FetchedRowTracker.__init__(self, **kwargs)
self.conn_id = None
self.auto_rollback = False
self.auto_commit = True
def get_sql(self):
def get_sql(self, default_conn=None):
return None
def can_edit(self):

View File

@ -423,7 +423,7 @@ input.editor-checkbox:focus {
/* To highlight all newly inserted rows */
.grid-canvas .new_row {
background: #dff0d7;
background: #dff0d7 !important;
}
/* To highlight all the updated rows */
@ -433,7 +433,7 @@ input.editor-checkbox:focus {
/* To highlight row at fault */
.grid-canvas .new_row.error, .grid-canvas .updated_row.error {
background: #f2dede;
background: #f2dede !important;
}
/* Disabled row */
@ -460,6 +460,11 @@ input.editor-checkbox:focus {
background-color: #2C76B4;
}
.slick-cell span[data-cell-type="row-header-selector"] {
display: block;
text-align: right;
}
#datagrid div.slick-header.ui-state-default {
background: #ffffff;
border-bottom: none;
@ -481,7 +486,9 @@ input.editor-checkbox:focus {
.select-all-icon {
margin-left: 9px;
margin-right: 9px;
vertical-align: bottom;
float: right;
}
.slick-cell, .slick-headerrow-column {

View File

@ -101,6 +101,12 @@ class BaseConnection(object):
- Implement this method to execute the given query and returns the result
as an array of dict (column name -> value) format.
* def async_fetchmany_2darray(records=-1, formatted_exception_msg=False):
- Implement this method to retrieve result of asynchronous connection and
polling with no_result flag set to True.
This returns the result as a 2 dimensional array.
If records is -1 then fetchmany will behave as fetchall.
* connected()
- Implement this method to get the status of the connection. It should
return True for connected, otherwise False
@ -133,7 +139,7 @@ class BaseConnection(object):
- Implement this method to wait for asynchronous connection with timeout.
This must be a non blocking call.
* poll(formatted_exception_msg)
* poll(formatted_exception_msg, no_result)
- Implement this method to poll the data of query running on asynchronous
connection.
@ -179,6 +185,10 @@ class BaseConnection(object):
def execute_dict(self, query, params=None, formatted_exception_msg=False):
pass
@abstractmethod
def async_fetchmany_2darray(self, records=-1, formatted_exception_msg=False):
pass
@abstractmethod
def connected(self):
pass
@ -208,7 +218,7 @@ class BaseConnection(object):
pass
@abstractmethod
def poll(self, formatted_exception_msg=True):
def poll(self, formatted_exception_msg=True, no_result=False):
pass
@abstractmethod

View File

@ -1079,6 +1079,55 @@ Failed to execute query (execute_void) for the server #{server_id} - {conn_id}
return True, {'columns': columns, 'rows': rows}
def async_fetchmany_2darray(self, records=2000, formatted_exception_msg=False):
"""
User should poll and check if status is ASYNC_OK before calling this
function
Args:
records: no of records to fetch. use -1 to fetchall.
formatted_exception_msg:
Returns:
"""
cur = self.__async_cursor
if not cur:
return False, gettext(
"Cursor could not be found for the async connection."
)
if self.conn.isexecuting():
return False, gettext(
"Asynchronous query execution/operation underway."
)
if self.row_count > 0:
result = []
# For DDL operation, we may not have result.
#
# Because - there is not direct way to differentiate DML and
# DDL operations, we need to rely on exception to figure
# that out at the moment.
try:
if records == -1:
res = cur.fetchall()
else:
res = cur.fetchmany(records)
for row in res:
new_row = []
for col in self.column_info:
new_row.append(row[col['name']])
result.append(new_row)
except psycopg2.ProgrammingError as e:
result = None
else:
# User performed operation which dose not produce record/s as
# result.
# for eg. DDL operations.
return True, None
return True, result
def connected(self):
if self.conn:
if not self.conn.closed:
@ -1226,7 +1275,7 @@ Failed to reset the connection to the server due to following error:
"poll() returned %s from _wait_timeout function" % state
)
def poll(self, formatted_exception_msg=False):
def poll(self, formatted_exception_msg=False, no_result=False):
"""
This function is a wrapper around connection's poll function.
It internally uses the _wait_timeout method to poll the
@ -1236,6 +1285,7 @@ Failed to reset the connection to the server due to following error:
Args:
formatted_exception_msg: if True then function return the formatted
exception message, otherwise error string.
no_result: If True then only poll status will be returned.
"""
cur = self.__async_cursor
@ -1291,23 +1341,23 @@ Failed to reset the connection to the server due to following error:
pos += 1
self.row_count = cur.rowcount
if not no_result:
if cur.rowcount > 0:
result = []
# For DDL operation, we may not have result.
#
# Because - there is not direct way to differentiate DML and
# DDL operations, we need to rely on exception to figure
# that out at the moment.
try:
for row in cur:
new_row = []
for col in self.column_info:
new_row.append(row[col['name']])
result.append(new_row)
if cur.rowcount > 0:
result = []
# For DDL operation, we may not have result.
#
# Because - there is not direct way to differentiate DML and
# DDL operations, we need to rely on exception to figure that
# out at the moment.
try:
for row in cur:
new_row = []
for col in self.column_info:
new_row.append(row[col['name']])
result.append(new_row)
except psycopg2.ProgrammingError:
result = None
except psycopg2.ProgrammingError:
result = None
return status, result

View File

@ -85,7 +85,7 @@ class PgadminPage:
if 'menu-item' == str(menu_item.get_attribute('class')):
break
time.sleep(0.1)
time.sleep(0.1)
else:
assert False, "'Tools -> Query Tool' menu did not enable."
@ -144,7 +144,6 @@ class PgadminPage:
except WebDriverException:
return
def find_by_xpath(self, xpath):
return self.wait_for_element(lambda driver: driver.find_element_by_xpath(xpath))
@ -251,6 +250,20 @@ class PgadminPage:
self._wait_for("spinner to disappear", spinner_has_disappeared)
def wait_for_query_tool_loading_indicator_to_disappear(self):
def spinner_has_disappeared(driver):
try:
driver.find_element_by_xpath(
"//*[@id='fetching_data' and @class='hide']"
)
return False
except NoSuchElementException:
# wait for loading indicator disappear animation to complete.
time.sleep(0.5)
return True
self._wait_for("spinner to disappear", spinner_has_disappeared)
def wait_for_app(self):
def page_shows_app(driver):
if driver.title == self.app_config.APP_NAME:
@ -266,19 +279,3 @@ class PgadminPage:
timeout = self.timeout
return WebDriverWait(self.driver, timeout, 0.01).until(condition_met_function,
"Timed out waiting for " + waiting_for_message)
def wait_for_element_to_stale(self, xpath):
# Reference: http://www.obeythetestinggoat.com/
# how-to-get-selenium-to-wait-for-page-load-after-a-click.html
el = self.driver.find_element_by_xpath(xpath)
def element_has_gone_stale(driver):
try:
# poll an arbitrary element
el.find_elements_by_id('element-dont-exist')
return False
except StaleElementReferenceException:
return True
self._wait_for("element to attach to the page document",
element_has_gone_stale)

View File

@ -16,16 +16,16 @@ import clipboard from '../../../pgadmin/static/js/selection/clipboard';
import copyData from '../../../pgadmin/static/js/selection/copy_data';
import RangeSelectionHelper from 'sources/selection/range_selection_helper';
import XCellSelectionModel from 'sources/selection/xcell_selection_model';
describe('copyData', function () {
var grid, sqlEditor, gridContainer, buttonPasteRow;
var SlickGrid;
beforeEach(function () {
SlickGrid = Slick.Grid;
var data = [[1, 'leopord', '12'],
[2, 'lion', '13'],
[3, 'puma', '9']];
var data = [{'id': 1, 'brand':'leopord', 'size':'12', '__temp_PK': '123'},
{'id': 2, 'brand':'lion', 'size':'13', '__temp_PK': '456'},
{'id': 3, 'brand':'puma', 'size':'9', '__temp_PK': '789'}],
dataView = new Slick.Data.DataView();
var columns = [
{
@ -37,6 +37,7 @@ describe('copyData', function () {
},
{
name: 'id',
field: 'id',
pos: 0,
label: 'id<br> numeric',
cell: 'number',
@ -44,6 +45,7 @@ describe('copyData', function () {
type: 'numeric',
}, {
name: 'brand',
field: 'brand',
pos: 1,
label: 'flavor<br> character varying',
cell: 'string',
@ -51,24 +53,26 @@ describe('copyData', function () {
type: 'character varying',
}, {
name: 'size',
field: 'size',
pos: 2,
label: 'size<br> numeric',
cell: 'number',
can_edit: false,
type: 'numeric',
},
]
;
gridContainer = $('<div id=\'grid\'></div>');
];
gridContainer = $('<div id="grid"></div>');
$('body').append(gridContainer);
buttonPasteRow = $('<button id=\'btn-paste-row\' disabled></button>');
buttonPasteRow = $('<button id="btn-paste-row" disabled></button>');
$('body').append(buttonPasteRow);
grid = new SlickGrid('#grid', data, columns, {});
grid = new SlickGrid('#grid', dataView, columns, {});
dataView.setItems(data, '__temp_PK');
grid.setSelectionModel(new XCellSelectionModel());
sqlEditor = {slickgrid: grid};
});
afterEach(function () {
afterEach(function() {
grid.destroy();
gridContainer.remove();
buttonPasteRow.remove();
});

View File

@ -134,17 +134,19 @@ describe('RangeBoundaryNavigator', function () {
describe('#rangesToCsv', function () {
var data, columnDefinitions, ranges;
beforeEach(function () {
data = [[1, 'leopard', '12'],
[2, 'lion', '13'],
[3, 'cougar', '9'],
[4, 'tiger', '10']];
columnDefinitions = [{name: 'id', pos: 0}, {name: 'animal', pos: 1}, {name: 'size', pos: 2}];
data = [{'id':1, 'animal':'leopard', 'size':'12'},
{'id':2, 'animal':'lion', 'size':'13'},
{'id':3, 'animal':'cougar', 'size':'9'},
{'id':4, 'animal':'tiger', 'size':'10'}];
columnDefinitions = [{name: 'id', field: 'id', pos: 0},
{name: 'animal', field: 'animal', pos: 1},
{name: 'size', field: 'size', pos: 2}];
ranges = [new Slick.Range(0, 0, 0, 2), new Slick.Range(3, 0, 3, 2)];
});
it('returns csv for the provided ranges', function () {
var csvResult = rangeBoundaryNavigator.rangesToCsv(data, columnDefinitions, ranges);
expect(csvResult).toEqual('1,\'leopard\',\'12\'\n4,\'tiger\',\'10\'');
});
@ -158,10 +160,10 @@ describe('RangeBoundaryNavigator', function () {
describe('when there is an extra column with checkboxes', function () {
beforeEach(function () {
columnDefinitions = [{name: 'not-a-data-column'}, {name: 'id', pos: 0}, {name: 'animal', pos: 1}, {
name: 'size',
pos: 2,
}];
columnDefinitions = [{name: 'not-a-data-column'},
{name: 'id', field: 'id', pos: 0},
{name: 'animal', field: 'animal', pos: 1},
{name: 'size', field: 'size',pos: 2}];
ranges = [new Slick.Range(0, 0, 0, 3), new Slick.Range(3, 0, 3, 3)];
});

View File

@ -6,7 +6,6 @@
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import $ from 'jquery';
import Slick from 'slickgrid';
@ -23,7 +22,7 @@ describe('RowSelector', function () {
UP: 38,
DOWN: 40,
};
var container, data, columnDefinitions, grid, cellSelectionModel;
var container, dataView, columnDefinitions, grid, cellSelectionModel;
var SlickGrid = Slick.Grid;
beforeEach(function () {
@ -43,14 +42,15 @@ describe('RowSelector', function () {
pos: 1,
}];
dataView = new Slick.Data.DataView();
var rowSelector = new RowSelector();
data = [];
var data = [];
for (var i = 0; i < 10; i++) {
data.push(['some-value-' + i, 'second value ' + i]);
data.push({'some-column-name':'some-value-' + i, 'second column':'second value ' + i});
}
columnDefinitions = rowSelector.getColumnDefinitions(columnDefinitions);
grid = new SlickGrid(container, data, columnDefinitions);
dataView.setItems(data, 'some-column-name');
grid = new SlickGrid(container, dataView, columnDefinitions);
grid.registerPlugin(new ActiveCellCapture());
cellSelectionModel = new XCellSelectionModel();
grid.setSelectionModel(cellSelectionModel);

View File

@ -7,236 +7,247 @@
//
//////////////////////////////////////////////////////////////
define([
'jquery',
'underscore',
'sources/selection/set_staged_rows',
], function ($, _, SetStagedRows) {
describe('set_staged_rows', function () {
var sqlEditorObj, gridSpy, deleteButton, copyButton, selectionSpy;
beforeEach(function () {
gridSpy = jasmine.createSpyObj('gridSpy', ['getData', 'getCellNode', 'getColumns']);
gridSpy.getData.and.returnValue([
{0: 'one', 1: 'two', __temp_PK: '123'},
{0: 'three', 1: 'four', __temp_PK: '456'},
{0: 'five', 1: 'six', __temp_PK: '789'},
{0: 'seven', 1: 'eight', __temp_PK: '432'},
]);
gridSpy.getColumns.and.returnValue([
import $ from 'jquery';
import 'slickgrid.grid';
import Slick from 'slickgrid';
import SetStagedRows from 'sources/selection/set_staged_rows';
describe('set_staged_rows', function () {
var sqlEditorObj, gridSpy, deleteButton, copyButton, selectionSpy;
beforeEach(function () {
var data = [{'a pk column': 'one', 'some column': 'two', '__temp_PK': '123'},
{'a pk column': 'three', 'some column': 'four', '__temp_PK': '456'},
{'a pk column': 'five', 'some column': 'six', '__temp_PK': '789'},
{'a pk column': 'seven', 'some column': 'eight', '__temp_PK': '432'}],
dataView = new Slick.Data.DataView();
dataView.setItems(data, '__temp_PK');
gridSpy = jasmine.createSpyObj('gridSpy', ['getData', 'getCellNode', 'getColumns']);
gridSpy.getData.and.returnValue(dataView);
gridSpy.getColumns.and.returnValue([
{
name: 'a pk column',
field: 'a pk column',
pos: 0,
selectable: true,
}, {
name: 'some column',
field: 'some column',
pos: 1,
selectable: true,
},
]);
selectionSpy = jasmine.createSpyObj('selectionSpy', ['setSelectedRows', 'getSelectedRanges']);
deleteButton = $('<button id="btn-delete-row"></button>');
copyButton = $('<button id="btn-copy-row"></button>');
sqlEditorObj = {
grid: gridSpy,
editor: {
handler: {
data_store: {
staged_rows: {'456': {}},
},
can_edit: false,
},
},
keys: null,
selection: selectionSpy,
columns: [
{
name: 'a pk column',
field: 'a pk column',
pos: 0,
selectable: true,
}, {
},
{
name: 'some column',
field: 'some column',
pos: 1,
selectable: true,
},
]);
],
client_primary_key: '__temp_PK',
};
selectionSpy = jasmine.createSpyObj('selectionSpy', ['setSelectedRows', 'getSelectedRanges']);
$('body').append(deleteButton);
$('body').append(copyButton);
deleteButton = $('<button id="btn-delete-row"></button>');
copyButton = $('<button id="btn-copy-row"></button>');
deleteButton.prop('disabled', true);
copyButton.prop('disabled', true);
sqlEditorObj = {
grid: gridSpy,
editor: {
handler: {
data_store: {
staged_rows: {'456': {}},
},
can_edit: false,
},
},
keys: null,
selection: selectionSpy,
columns: [
{
name: 'a pk column',
pos: 0,
},
{
name: 'some column',
pos: 1,
},
],
};
selectionSpy = jasmine.createSpyObj('selectionSpy', [
'setSelectedRows',
'getSelectedRanges',
]);
});
$('body').append(deleteButton);
$('body').append(copyButton);
deleteButton.prop('disabled', true);
copyButton.prop('disabled', true);
selectionSpy = jasmine.createSpyObj('selectionSpy', [
'setSelectedRows',
'getSelectedRanges',
]);
});
afterEach(function () {
copyButton.remove();
deleteButton.remove();
});
describe('when no full rows are selected', function () {
describe('when nothing is selected', function () {
beforeEach(function () {
selectionSpy.getSelectedRanges.and.returnValue([]);
sqlEditorObj.selection = selectionSpy;
SetStagedRows.call(sqlEditorObj, {}, {});
});
it('should disable the delete row button', function () {
expect($('#btn-delete-row').prop('disabled')).toBeTruthy();
});
it('should disable the copy row button', function () {
expect($('#btn-copy-row').prop('disabled')).toBeTruthy();
});
it('should clear staged rows', function () {
expect(sqlEditorObj.editor.handler.data_store.staged_rows).toEqual({});
});
});
describe('when there is a selection', function () {
beforeEach(function () {
var range = {
fromCell: 0,
toCell: 0,
fromRow: 1,
toRow: 1,
};
selectionSpy.getSelectedRanges.and.returnValue([range]);
sqlEditorObj.selection = selectionSpy;
SetStagedRows.call(sqlEditorObj, {}, {});
});
it('should disable the delete row button', function () {
expect($('#btn-delete-row').prop('disabled')).toBeTruthy();
});
it('should disable the copy row button', function () {
expect($('#btn-copy-row').prop('disabled')).toBeFalsy();
});
it('should clear staged rows', function () {
expect(sqlEditorObj.editor.handler.data_store.staged_rows).toEqual({});
});
});
});
describe('when 2 full rows are selected', function () {
afterEach(function () {
copyButton.remove();
deleteButton.remove();
});
describe('when no full rows are selected', function () {
describe('when nothing is selected', function () {
beforeEach(function () {
var range1 = {
selectionSpy.getSelectedRanges.and.returnValue([]);
sqlEditorObj.selection = selectionSpy;
SetStagedRows.call(sqlEditorObj, {}, {});
});
it('should disable the delete row button', function () {
expect($('#btn-delete-row').prop('disabled')).toBeTruthy();
});
it('should disable the copy row button', function () {
expect($('#btn-copy-row').prop('disabled')).toBeTruthy();
});
it('should clear staged rows', function () {
expect(sqlEditorObj.editor.handler.data_store.staged_rows).toEqual({});
});
});
describe('when there is a selection', function () {
beforeEach(function () {
var range = {
fromCell: 0,
toCell: 1,
toCell: 0,
fromRow: 1,
toRow: 1,
};
var range2 = {
fromCell: 0,
toCell: 1,
fromRow: 2,
toRow: 2,
};
selectionSpy.getSelectedRanges.and.returnValue([range1, range2]);
selectionSpy.getSelectedRanges.and.returnValue([range]);
sqlEditorObj.selection = selectionSpy;
SetStagedRows.call(sqlEditorObj, {}, {});
});
describe('when table does not have primary keys', function () {
it('should enable the copy row button', function () {
it('should disable the delete row button', function () {
expect($('#btn-delete-row').prop('disabled')).toBeTruthy();
});
it('should disable the copy row button', function () {
expect($('#btn-copy-row').prop('disabled')).toBeFalsy();
});
it('should clear staged rows', function () {
expect(sqlEditorObj.editor.handler.data_store.staged_rows).toEqual({});
});
});
});
describe('when 2 full rows are selected', function () {
beforeEach(function () {
var range1 = {
fromCell: 0,
toCell: 1,
fromRow: 1,
toRow: 1,
};
var range2 = {
fromCell: 0,
toCell: 1,
fromRow: 2,
toRow: 2,
};
selectionSpy.getSelectedRanges.and.returnValue([range1, range2]);
sqlEditorObj.selection = selectionSpy;
});
describe('when table does not have primary keys', function () {
it('should enable the copy row button', function () {
SetStagedRows.call(sqlEditorObj, {}, {});
expect($('#btn-copy-row').prop('disabled')).toBeFalsy();
});
it('should not enable the delete row button', function () {
SetStagedRows.call(sqlEditorObj, {}, {});
expect($('#btn-delete-row').prop('disabled')).toBeTruthy();
});
it('should update staged rows with the __temp_PK value of the new Selected Rows', function () {
SetStagedRows.call(sqlEditorObj, {}, {});
expect(sqlEditorObj.editor.handler.data_store.staged_rows).toEqual({'456': {}, '789': {}});
});
describe('the user can edit', function () {
it('should enable the delete row button', function () {
sqlEditorObj.editor.handler.can_edit = true;
SetStagedRows.call(sqlEditorObj, {}, {});
expect($('#btn-copy-row').prop('disabled')).toBeFalsy();
expect($('#btn-delete-row').prop('disabled')).toBeFalsy();
});
});
});
describe('when table has primary keys', function () {
beforeEach(function () {
sqlEditorObj.keys = {'a pk column': 'varchar'};
sqlEditorObj.editor.handler.data_store.staged_rows = {'456': {'a pk column': 'three'}};
});
describe('selected rows have primary key', function () {
it('should set the staged rows correctly', function () {
SetStagedRows.call(sqlEditorObj, {}, {});
expect(sqlEditorObj.editor.handler.data_store.staged_rows).toEqual(
{'456': {'a pk column': 'three'}, '789': {'a pk column': 'five'}});
});
it('should not enable the delete row button', function () {
it('should not clear selected rows in Cell Selection Model', function () {
SetStagedRows.call(sqlEditorObj, {}, {});
expect($('#btn-delete-row').prop('disabled')).toBeTruthy();
});
it('should update staged rows with the __temp_PK value of the new Selected Rows', function () {
SetStagedRows.call(sqlEditorObj, {}, {});
expect(sqlEditorObj.editor.handler.data_store.staged_rows).toEqual({'456': {}, '789': {}});
});
describe('the user can edit', function () {
it('should enable the delete row button', function () {
sqlEditorObj.editor.handler.can_edit = true;
SetStagedRows.call(sqlEditorObj, {}, {});
expect($('#btn-delete-row').prop('disabled')).toBeFalsy();
});
expect(sqlEditorObj.selection.setSelectedRows).not.toHaveBeenCalledWith();
});
});
describe('when table has primary keys', function () {
describe('selected rows missing primary key', function () {
beforeEach(function () {
sqlEditorObj.keys = {'a pk column': 'varchar'};
sqlEditorObj.editor.handler.data_store.staged_rows = {'456': {0: 'three'}};
var data = [{'a pk column': 'one', 'some column': 'two', '__temp_PK': '123'},
{'some column': 'four', '__temp_PK': '456'},
{'some column': 'six', '__temp_PK': '789'},
{'a pk column': 'seven', 'some column': 'eight', '__temp_PK': '432'}],
dataView = new Slick.Data.DataView();
dataView.setItems(data, '__temp_PK');
gridSpy.getData.and.returnValue(dataView);
});
describe('selected rows have primary key', function () {
it('should set the staged rows correctly', function () {
SetStagedRows.call(sqlEditorObj, {}, {});
expect(sqlEditorObj.editor.handler.data_store.staged_rows).toEqual(
{'456': {0: 'three'}, '789': {0: 'five'}});
});
it('should clear the staged rows', function () {
SetStagedRows.call(sqlEditorObj, {}, {});
expect(sqlEditorObj.editor.handler.data_store.staged_rows).toEqual({});
});
it('should not clear selected rows in Cell Selection Model', function () {
SetStagedRows.call(sqlEditorObj, {}, {});
expect(sqlEditorObj.selection.setSelectedRows).not.toHaveBeenCalledWith();
it('should clear selected rows in Cell Selection Model', function () {
SetStagedRows.call(sqlEditorObj, {}, {});
expect(sqlEditorObj.selection.setSelectedRows).toHaveBeenCalledWith([]);
});
});
describe('when the selected row is a new row', function () {
var parentDiv;
beforeEach(function () {
var childDiv = $('<div></div>');
parentDiv = $('<div class="new_row"></div>');
parentDiv.append(childDiv);
$('body').append(parentDiv);
gridSpy.getCellNode.and.returnValue(childDiv);
SetStagedRows.call(sqlEditorObj, {}, {});
});
afterEach(function () {
parentDiv.remove();
});
it('should not clear the staged rows', function () {
expect(sqlEditorObj.editor.handler.data_store.staged_rows).toEqual({
'456': {'a pk column': 'three'},
'789': {'a pk column': 'five'},
});
});
describe('selected rows missing primary key', function () {
beforeEach(function () {
gridSpy.getData.and.returnValue([
{0: 'one', 1: 'two', __temp_PK: '123'},
{1: 'four', __temp_PK: '456'},
{1: 'six', __temp_PK: '789'},
{0: 'seven', 1: 'eight', __temp_PK: '432'},
]);
});
it('should clear the staged rows', function () {
SetStagedRows.call(sqlEditorObj, {}, {});
expect(sqlEditorObj.editor.handler.data_store.staged_rows).toEqual({});
});
it('should clear selected rows in Cell Selection Model', function () {
SetStagedRows.call(sqlEditorObj, {}, {});
expect(sqlEditorObj.selection.setSelectedRows).toHaveBeenCalledWith([]);
});
});
describe('when the selected row is a new row', function () {
var parentDiv;
beforeEach(function () {
var childDiv = $('<div></div>');
parentDiv = $('<div class="new_row"></div>');
parentDiv.append(childDiv);
$('body').append(parentDiv);
gridSpy.getCellNode.and.returnValue(childDiv);
SetStagedRows.call(sqlEditorObj, {}, {});
});
afterEach(function () {
parentDiv.remove();
});
it('should not clear the staged rows', function () {
expect(sqlEditorObj.editor.handler.data_store.staged_rows).toEqual({
'456': {0: 'three'},
'789': {0: 'five'},
});
});
it('should not clear selected rows in Cell Selection Model', function () {
expect(sqlEditorObj.selection.setSelectedRows).not.toHaveBeenCalled();
});
it('should not clear selected rows in Cell Selection Model', function () {
expect(sqlEditorObj.selection.setSelectedRows).not.toHaveBeenCalled();
});
});
});
});
});

View File

@ -33,14 +33,17 @@ describe('XCellSelectionModel', function () {
}, {
id: '1',
name: 'some-column-name',
field: 'some-column-name',
pos: 0,
}, {
id: 'second-column-id',
name: 'second column',
field: 'second column',
pos: 1,
}, {
id: 'third-column-id',
name: 'third column',
field: 'third column',
pos: 2,
},
];
@ -52,13 +55,15 @@ describe('XCellSelectionModel', function () {
'second column': 'second value ' + i,
'third column': 'third value ' + i,
'fourth column': 'fourth value ' + i,
'__temp_PK': '123' + i,
});
}
container = $('<div></div>');
var dataView = new Slick.Data.DataView();
container.height(9999);
container.width(9999);
grid = new SlickGrid(container, data, columns);
dataView.setItems(data, '__temp_PK');
grid = new SlickGrid(container, dataView, columns);
grid.setSelectionModel(new XCellSelectionModel());
$('body').append(container);
});

View File

@ -28,21 +28,22 @@ describe('#handleQueryOutputKeyboardEvent', function () {
metaKey: false,
which: -1,
keyCode: -1,
preventDefault: jasmine.createSpy('preventDefault'),
preventDefault: jasmine.createSpy('preventDefault')
};
var data = [['', '0,0-cell-content', '0,1-cell-content'],
['', '1,0-cell-content', '1,1-cell-content'],
['', '2,0-cell-content', '2,1-cell-content']];
var columnDefinitions = [{name: 'checkboxColumn'}, {pos: 1, name: 'firstColumn'}, {
pos: 2,
name: 'secondColumn',
}];
grid = new SlickGrid($('<div></div>'), data, columnDefinitions);
grid.setSelectionModel(new XCellSelectionModel());
var data = [{'checkboxColumn': '', 'firstColumn': '0,0-cell-content', 'secondColumn': '0,1-cell-content', '__temp_PK': '123'},
{'checkboxColumn': '', 'firstColumn': '1,0-cell-content', 'secondColumn': '1,1-cell-content', '__temp_PK': '456'},
{'checkboxColumn': '', 'firstColumn': '2,0-cell-content', 'secondColumn': '2,1-cell-content', '__temp_PK': '789'}],
columnDefinitions = [{name: 'checkboxColumn'},
{pos: 1, name: 'firstColumn', field: 'firstColumn'},
{ pos: 2, name: 'secondColumn', field: 'secondColumn'}],
dataView = new Slick.Data.DataView();
grid = new Slick.Grid($('<div></div>'), dataView, columnDefinitions);
grid.setSelectionModel(new XCellSelectionModel());
dataView.setItems(data, '__temp_PK');
slickEvent = {
grid: grid,
grid: grid
};
spyOn(clipboard, 'copyTextToClipboard');