diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/external_tables/__init__.py new file mode 100644 index 00000000000..137c6c446cf --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/__init__.py @@ -0,0 +1,275 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Implements External Tables Node""" +import os +from functools import wraps +from gettext import gettext + +from flask import render_template + +from config import PG_DEFAULT_DRIVER +from pgadmin.browser.collection import CollectionNodeModule +from pgadmin.browser.server_groups.servers import databases +from pgadmin.browser.server_groups.servers.databases \ + .external_tables.mapping_utils import map_execution_location +from pgadmin.browser.server_groups.servers.databases \ + .external_tables.properties import Properties, \ + PropertiesTableNotFoundException, PropertiesException +from pgadmin.browser.server_groups.servers.databases \ + .external_tables.reverse_engineer_ddl import ReverseEngineerDDL +from pgadmin.browser.utils import PGChildNodeView +from pgadmin.utils.ajax import make_json_response, make_response, \ + internal_server_error +from pgadmin.utils.compile_template_name import compile_template_path +from pgadmin.utils.driver import get_driver + + +class ExternalTablesModule(CollectionNodeModule): + """ + class ExternalTablesModule(CollectionNodeModule) + + A module class for External Tables node derived from + CollectionNodeModule. + + Methods: + ------- + * __init__(*args, **kwargs) + - Method is used to initialize the External Tables module + and it's base module. + + * get_nodes(gid, sid, did) + - Method is used to generate the browser collection node. + + * script_load() + - Load the module script for External Tables, when any of + the database node is initialized. + """ + + NODE_TYPE = 'external_table' + COLLECTION_LABEL = gettext("External Tables") + + def __init__(self, *args, **kwargs): + """ + Method is used to initialize the External tables module and + it's base module. + + Args: + *args: + **kwargs: + """ + + super(ExternalTablesModule, self).__init__(*args, **kwargs) + self.max_ver = 0 + + def get_nodes(self, gid, sid, did): + yield self.generate_browser_collection_node(did) + + @property + def script_load(self): + """ + Load the module script for External tables, + when any of the database node is initialized. + + Returns: node type of the database module. + """ + return databases.DatabaseModule.NODE_TYPE + + @property + def module_use_template_javascript(self): + """ + Returns whether Jinja2 template is used for generating the javascript + module. + """ + return False + + +blueprint = ExternalTablesModule(__name__) + + +class ExternalTablesView(PGChildNodeView): + node_type = blueprint.node_type + + parent_ids = [ + {'type': 'int', 'id': 'server_group_id'}, + {'type': 'int', 'id': 'server_id'}, + {'type': 'int', 'id': 'database_id'} + ] + + ids = [ + {'type': 'int', 'id': 'external_table_id'} + ] + + operations = dict({ + 'obj': [ + {'get': 'properties'} + ], + 'nodes': [{'get': 'node'}, {'get': 'nodes'}], + 'sql': [{'get': 'sql'}], + 'children': [{'get': 'children'}] + }) + + def check_precondition(function_wrapped): + """ + This function will behave as a decorator which will checks + database connection before running view, it will also attaches + manager,conn & template_path properties to self + """ + + @wraps(function_wrapped) + def wrap(*args, **kwargs): + # Here args[0] will hold self & kwargs will hold gid,sid,did + self = args[0] + self.manager = get_driver(PG_DEFAULT_DRIVER).connection_manager( + kwargs['server_id'] + ) + self.connection = self.manager.connection( + did=kwargs['database_id'] + ) + self.sql_template_path = compile_template_path( + 'sql/', + self.manager.server_type, + self.manager.sversion + ) + + return function_wrapped(*args, **kwargs) + + return wrap + + def __init__(self, *args, **kwargs): + super(ExternalTablesView, self).__init__(*args, **kwargs) + self.connection = None + self.manager = None + self.sql_template_path = None + + @check_precondition + def nodes(self, server_group_id, server_id, database_id): + """ + This function will used to create all the child node within that + collection. + Here it will create all the foreign data wrapper node. + + Args: + server_group_id: Server Group ID + server_id: Server ID + database_id: Database ID + """ + sql_statement = render_template( + os.path.join(self.sql_template_path, 'list.sql') + ) + + result = self.get_external_tables(database_id, sql_statement) + + if type(result) is not list: + return result + + return make_json_response( + data=result, + status=200 + ) + + @check_precondition + def node(self, server_group_id, server_id, database_id, external_table_id): + """ + This function will used to create all the child node within that + collection. + Here it will create all the foreign data wrapper node. + + Args: + server_group_id: Server Group ID + server_id: Server ID + database_id: Database ID + external_table_id: External Table ID + """ + sql_statement = render_template( + template_name_or_list=os.path.join( + self.sql_template_path, + 'node.sql' + ), + external_table_id=external_table_id + ) + result = self.get_external_tables(database_id, sql_statement) + + if type(result) is not list: + return result + + if len(result) == 0: + return make_json_response( + data=gettext('Could not find the external table.'), + status=404 + ) + + return make_json_response( + data=result[0], + status=200 + ) + + @check_precondition + def sql(self, server_group_id, server_id, database_id, external_table_id): + """ + This function will used to create all the child node within that + collection. + Here it will create all the foreign data wrapper node. + + Args: + server_group_id: Server Group ID + server_id: Server ID + database_id: Database ID + external_table_id: External Table ID + """ + sql = ReverseEngineerDDL(self.sql_template_path, + render_template, + self.connection, server_group_id, server_id, + database_id).execute(external_table_id) + + return make_response( + sql.strip('\n') + ) + + @check_precondition + def properties(self, server_group_id, server_id, database_id, + external_table_id): + try: + response = Properties(render_template, self.connection, + self.sql_template_path).retrieve( + external_table_id) + return make_response( + response=response, + status=200) + except PropertiesTableNotFoundException: + return make_json_response( + data=gettext('Could not find the external table.'), + status=404 + ) + except PropertiesException as exception: + return exception.response_object + + def children(self, **kwargs): + return make_json_response(data=[]) + + def get_external_tables(self, database_id, sql_statement): + status, external_tables = self.connection \ + .execute_2darray(sql_statement) + if not status: + return internal_server_error(errormsg=external_tables) + + icon_css_class = 'icon-external_table' + result = [] + for external_table in external_tables['rows']: + result.append(self.blueprint.generate_browser_node( + external_table['oid'], + database_id, + external_table['name'], + inode=False, + icon=icon_css_class + )) + return result + + +ExternalTablesView.register_node_view(blueprint) diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/actions/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/external_tables/actions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/actions/get_all_nodes.py b/web/pgadmin/browser/server_groups/servers/databases/external_tables/actions/get_all_nodes.py new file mode 100644 index 00000000000..a06bc0b3f2d --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/actions/get_all_nodes.py @@ -0,0 +1,4 @@ + +class GetAllNodes: + def execute(self): + pass diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/mapping_utils.py b/web/pgadmin/browser/server_groups/servers/databases/external_tables/mapping_utils.py new file mode 100644 index 00000000000..b4fe89a404f --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/mapping_utils.py @@ -0,0 +1,165 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## +import re + + +def map_column_from_database(column_information): + return { + 'name': column_information['name'], + 'type': column_information['cltype'] + } + + +def map_table_information_from_database(table_information): + format_type = map_format_type(table_information['fmttype']) + return { + 'uris': sql_array_notation_to_array(table_information['urilocation']), + 'isWeb': is_web_table( + table_information['urilocation'], + table_information['command'] + ), + 'executionLocation': map_execution_location( + table_information['execlocation']), + 'formatType': format_type, + 'formatOptions': format_options(format_type, + table_information['fmtopts']), + 'command': table_information['command'], + 'rejectLimit': table_information['rejectlimit'], + 'rejectLimitType': table_information['rejectlimittype'], + 'errorTableName': table_information['errtblname'], + 'erroToFile': table_information['errortofile'], + 'pgEncodingToChar': table_information['pg_encoding_to_char'], + 'writable': table_information['writable'], + 'options': table_information['options'], + 'distribution': table_information['distribution'], + 'name': table_information['name'], + 'namespace': table_information['namespace'] + } + + +def map_execution_location(execution_location): + stripped_execution_location = execution_location[0].lstrip('{').rstrip('}') + if stripped_execution_location.startswith('HOST:'): + return { + 'type': 'host', + 'value': stripped_execution_location.replace('HOST:', '').strip() + } + elif stripped_execution_location == 'PER_HOST': + return {'type': 'per_host', 'value': None} + elif stripped_execution_location == "MASTER_ONLY": + return {'type': 'master_only', 'value': None} + elif stripped_execution_location == "ALL_SEGMENTS": + return {'type': 'all_segments', 'value': None} + elif stripped_execution_location.startswith("SEGMENT_ID:"): + return { + 'type': 'segment', + 'value': stripped_execution_location.replace('SEGMENT_ID:', '') + .strip() + } + elif stripped_execution_location.startswith("TOTAL_SEGS:"): + return { + 'type': 'segments', + 'value': stripped_execution_location.replace('TOTAL_SEGS:', '') + .strip() + } + + +def map_format_type(format_type): + if format_type == 'b': + return 'custom' + elif format_type == 'a': + return 'avro' + elif format_type == 't': + return 'text' + elif format_type == 'p': + return 'parquet' + else: + return 'csv' + + +def is_web_table(uris, command): + if uris is None and command is None: + return False + if command is not None: + return True + return re.search('^https?:\\/\\/', + sql_array_notation_to_array(uris)[0]) is not None + + +def format_options(format_type, options): + if options is None: + return None + if len(options) == 0: + return options + + result_options = tokenize_options(options) + all_keys = list(result_options.keys()) + all_keys.sort() + if format_type not in ['csv', 'text']: + return ','.join([ + '%s = %s' % (key, result_options[key]) for key in all_keys + ]) + else: + return ' '.join([ + '%s %s' % (key, result_options[key]) for key in all_keys + ]) + + +def sql_array_notation_to_array(sql_result): + if sql_result is None: + return None + if sql_result[0] == '{': + return sql_result[1:-1].split(',') + return sql_result + + +def tokenize_options(options): + in_key = True + in_value = False + current_key = '' + current_value = '' + tokens = {} + for index in range(0, len(options)): + if is_end_of_key(in_key, options, index, current_key): + in_key = False + elif is_not_end_of_key(in_key, index, options): + current_key += options[index] + elif is_start_of_value(in_value, index, options): + in_value = True + current_value = '' + elif is_end_of_value(in_value, index, options): + in_value = False + in_key = True + tokens[current_key] = '$$' + current_value + '$$' + current_key = '' + current_value = '' + elif in_value: + current_value += options[index] + return tokens + + +def found_apostrophe_inside_value(in_value, index, options): + return in_value and options[index] == '\'' + + +def is_end_of_value(in_value, index, options): + return in_value and options[index] == '\'' and ( + index == (len(options) - 1) or options[index + 1] == ' ') + + +def is_start_of_value(in_value, index, options): + return not in_value and options[index] == '\'' + + +def is_not_end_of_key(in_key, index, options): + return in_key and options[index] != ' ' + + +def is_end_of_key(in_key, options, index, current_key): + return in_key and options[index] == ' ' and len(current_key) > 0 diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/properties.py b/web/pgadmin/browser/server_groups/servers/databases/external_tables/properties.py new file mode 100644 index 00000000000..f0b6f229c39 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/properties.py @@ -0,0 +1,78 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## +from gettext import gettext +from os import path + +from pgadmin.browser.server_groups.servers.databases.external_tables import \ + map_execution_location +from pgadmin.utils.ajax import internal_server_error + + +class PropertiesException(Exception): + def __init__(self, response_object, *args): + super(PropertiesException, self).__init__(*args) + self.response_object = response_object + + +class PropertiesTableNotFoundException(PropertiesException): + def __init__(self, *args): + super(PropertiesException, self).__init__(None, *args) + + +class Properties: + def __init__(self, render_template, db_connection, sql_template_path): + self.render_template = render_template + self.db_connection = db_connection + self.sql_template_path = sql_template_path + + def retrieve(self, table_oid): + table_information_sql = self.render_template( + template_name_or_list=path.join(self.sql_template_path, + 'get_table_information.sql'), + table_oid=table_oid + ) + + (status, table_information_results) = \ + self.db_connection.execute_2darray(table_information_sql) + if not status: + raise PropertiesException( + internal_server_error(table_information_results)) + if len(table_information_results['rows']) != 1: + raise PropertiesTableNotFoundException() + + table_information_result = table_information_results['rows'][0] + execute_on = map_execution_location( + table_information_result['execlocation']) + execute_on_text = self.translate_execute_on_text(execute_on) + response = dict( + name=table_information_result['name'], + type=gettext('readable' if not table_information_result[ + 'writable'] else 'writable'), + format_type=table_information_result['pg_encoding_to_char'], + format_options=table_information_result['fmtopts'], + external_options=table_information_result['options'], + command=table_information_result['command'], + execute_on=execute_on_text, + ) + return response + + @staticmethod + def translate_execute_on_text(execute_on): + if execute_on['type'] == 'host': + return 'host %s' % execute_on['value'] + elif execute_on['type'] == 'per_host': + return 'per host' + elif execute_on['type'] == 'master_only': + return 'master segment' + elif execute_on['type'] == 'all_segments': + return 'all segments' + elif execute_on['type'] == 'segment': + return '%s segment' % execute_on['value'] + elif execute_on['type'] == 'segments': + return '%d segments' % execute_on['value'] diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/reverse_engineer_ddl.py b/web/pgadmin/browser/server_groups/servers/databases/external_tables/reverse_engineer_ddl.py new file mode 100644 index 00000000000..cf865089ac1 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/reverse_engineer_ddl.py @@ -0,0 +1,69 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## +from os import path + +from pgadmin.browser.server_groups.servers.databases\ + .external_tables.mapping_utils import \ + map_column_from_database, map_table_information_from_database + + +class ReverseEngineerDDLException(Exception): + pass + + +class ReverseEngineerDDL: + def __init__(self, sql_template_paths, + render_template, + database_connection, + server_group_id, server_id, database_id): + self.sql_template_path = sql_template_paths + self.render_template = render_template + self.database_connection = database_connection + + def execute(self, table_oid): + reverse_engineer_data = self.table_information(table_oid) + reverse_engineer_data['columns'] = self.find_columns(table_oid) + return self.render_template( + template_name_or_list=path.join(self.sql_template_path, + 'create.sql'), + table=reverse_engineer_data + ) + + def find_columns(self, table_oid): + columns_sql = self.render_template( + template_name_or_list=path.join(self.sql_template_path, + 'get_columns.sql'), + table_oid=table_oid + ) + + (status, column_result) = \ + self.database_connection.execute_2darray(columns_sql) + if not status: + raise ReverseEngineerDDLException(column_result) + + return list(map(map_column_from_database, column_result['rows'])) + + def table_information(self, table_oid): + table_information_sql = self.render_template( + template_name_or_list=path.join(self.sql_template_path, + 'get_table_information.sql'), + table_oid=table_oid + ) + + (status, table_information_result) = \ + self.database_connection.execute_2darray(table_information_sql) + if not status: + raise ReverseEngineerDDLException(table_information_result) + elif 'rows' not in table_information_result.keys() or len( + table_information_result['rows'] + ) == 0: + raise ReverseEngineerDDLException('Table not found') + + return map_table_information_from_database( + table_information_result['rows'][0]) diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/static/img/coll-external_table.svg b/web/pgadmin/browser/server_groups/servers/databases/external_tables/static/img/coll-external_table.svg new file mode 100644 index 00000000000..bcce32d6681 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/static/img/coll-external_table.svg @@ -0,0 +1 @@ +coll-foreign_data_wrapper \ No newline at end of file diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/static/img/external_table.svg b/web/pgadmin/browser/server_groups/servers/databases/external_tables/static/img/external_table.svg new file mode 100644 index 00000000000..90228c9635c --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/static/img/external_table.svg @@ -0,0 +1 @@ +coll-table \ No newline at end of file diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/create.sql new file mode 100644 index 00000000000..a7f64a5fc64 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/create.sql @@ -0,0 +1,60 @@ +{% if table.rejectLimitType == 'r' %} +{% set rejectionLimit = 'ROWS' %} +{% else %} +{% set rejectionLimit = 'PERCENT' %} +{% endif %} +CREATE {% if table.writable %}WRITABLE {% endif %}EXTERNAL {% if table.isWeb %}WEB {% endif %}TABLE {{conn|qtIdent(table.namespace, table.name)}}{% if table.columns and table.columns|length > 0 %}( +{% for c in table.columns %} +{% if c.name and c.type -%} +{% if loop.index != 1 %}, +{% endif %} + {{conn|qtIdent(c.name)}} {{c.type}} +{%- endif %} +{% endfor %} +) +{% else %} +() +{% endif %} +{% if table.command and table.command|length > 0 %} +EXECUTE $pgAdmin${{ table.command }}'$pgAdmin$ +{% else %} +LOCATION ( +{% for uri in table.uris %} +{% if loop.index != 1 -%}, +{% endif %} + '{{uri}}' +{%- endfor %} +) +{% endif %} +{% if not table.writable and table.executionLocation %} +{% if table.executionLocation.type == 'host' %} +ON HOST {{ table.executionLocation.value }} +{% elif table.executionLocation.type == 'per_host' %} +ON HOST +{% elif table.executionLocation.type == 'master_only' %} +ON MASTER +{% elif table.executionLocation.type == 'all_segments' %} +ON ALL +{% elif table.executionLocation.type == 'segment' %} +ON SEGMENT {{ table.executionLocation.value }} +{% elif table.executionLocation.type == 'segments' %} +ON {{ table.executionLocation.value }} +{% endif %} +{% endif %} +FORMAT '{{ table.formatType }}' ({{ table.formatOptions }}) +{% if table.options and table.options|length > 0 %} +OPTIONS ( +{{ table.options }} +) +{% endif %} +ENCODING '{{ table.pgEncodingToChar }}' +{% if table.rejectLimit and table.rejectLimit|length > 0 %} +{% if table.errorTableName and table.errorTableName|length > 0 %} +LOG ERRORS {% endif %}SEGMENT REJECT LIMIT {{ table.rejectLimit }} {{ rejectionLimit }} +{% endif %} +{% if table.writable and table.distribution %} +DISTRIBUTED BY ({% for attrnum in table.distribution %}{% if loop.index != 1 %}, {% endif %}{{ table.columns[attrnum-1].name }}{% endfor %}); +{% elif table.writable %} +DISTRIBUTED RANDOMLY; +{% else %}; +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/get_columns.sql b/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/get_columns.sql new file mode 100644 index 00000000000..9b8589ad73b --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/get_columns.sql @@ -0,0 +1,12 @@ +SELECT + a.attname AS name, format_type(a.atttypid, NULL) AS cltype, + quote_ident(n.nspname)||'.'||quote_ident(c.relname) as inheritedfrom, + c.oid as inheritedid +FROM + pg_class c +JOIN + pg_namespace n ON c.relnamespace=n.oid +JOIN + pg_attribute a ON a.attrelid = c.oid AND NOT a.attisdropped AND a.attnum > 0 +WHERE + c.oid = {{ table_oid }}::OID diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/get_table_information.sql b/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/get_table_information.sql new file mode 100644 index 00000000000..50420ea3d37 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/get_table_information.sql @@ -0,0 +1,22 @@ +SELECT x.urilocation, x.execlocation, x.fmttype, x.fmtopts, x.command, + x.rejectlimit, x.rejectlimittype, + (SELECT relname + FROM pg_catalog.pg_class + WHERE Oid=x.fmterrtbl) AS errtblname, + x.fmterrtbl = x.reloid AS errortofile , + pg_catalog.pg_encoding_to_char(x.encoding), + x.writable, + array_to_string(ARRAY( + SELECT pg_catalog.quote_ident(option_name) || ' ' || + pg_catalog.quote_literal(option_value) + FROM pg_options_to_table(x.options) + ORDER BY option_name + ), E',\n ') AS options, + gdp.attrnums AS distribution, + c.relname AS name, + nsp.nspname AS namespace +FROM pg_catalog.pg_exttable x, + pg_catalog.pg_class c + LEFT JOIN pg_catalog.pg_namespace nsp ON nsp.oid = c.relnamespace + LEFT JOIN gp_distribution_policy gdp ON gdp.localoid = c.oid +WHERE x.reloid = c.oid AND c.oid = {{ table_oid }}::oid; diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/list.sql b/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/list.sql new file mode 100644 index 00000000000..7490583faae --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/list.sql @@ -0,0 +1,6 @@ +SELECT pg_class.oid, relname as name +FROM pg_class +LEFT JOIN pg_namespace ON pg_namespace.oid=pg_class.relnamespace::oid +WHERE relkind = 'r' + AND relstorage = 'x' + AND pg_namespace.nspname not like 'gp_toolkit'; diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/node.sql b/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/node.sql new file mode 100644 index 00000000000..87cb86f9add --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/node.sql @@ -0,0 +1,5 @@ +SELECT pg_class.oid, relname as name +FROM pg_class +WHERE relkind = 'r' + AND relstorage = 'x' + AND pg_class.oid = {{ external_table_id }}::oid; diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_external_tables_module.py b/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_external_tables_module.py new file mode 100644 index 00000000000..f8c47ab377c --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_external_tables_module.py @@ -0,0 +1,99 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import sys + +import six + +from pgadmin.browser.server_groups.servers\ + .databases.external_tables import ExternalTablesModule +from pgadmin.utils.route import BaseTestGenerator + +if sys.version_info < (3, 3): + from mock import MagicMock, Mock +else: + from unittest.mock import MagicMock, Mock + + +class TestExternalTablesModule(BaseTestGenerator): + scenarios = [ + ('#BackendSupported When access the on a Postgresql Database, ' + 'it returns false', + dict( + test_type='backend-support', + manager=dict( + server_type='pg', + sversion=90100 + ), + expected_result=False, + )), + ('#BackendSupported When access the on a GreenPlum Database, ' + 'it returns true', + dict( + test_type='backend-support', + manager=dict( + server_type='gpdb', + sversion=82303 + ), + expected_result=True + )), + ('#get_nodes when trying to retrieve the node, ' + 'it should return true', + dict( + test_type='get-nodes', + function_parameters=dict( + gid=10, + sid=11, + did=12, + ), + expected_generate_browser_collection_node_called_with=12 + )), + ('#get_module_use_template_javascript when checking if need to ' + 'generate javascript from template, ' + 'it should return false', + dict( + test_type='template-javascript', + expected_result=False + )) + ] + + def runTest(self): + if self.test_type == 'backend-support': + self.__test_backend_support() + elif self.test_type == 'get-nodes': + self.__test_get_nodes() + elif self.test_type == 'template-javascript': + self.__test_template_javascript() + + def __test_backend_support(self): + manager = MagicMock() + manager.sversion = self.manager['sversion'] + manager.server_type = self.manager['server_type'] + module = ExternalTablesModule('something') + self.assertEquals( + self.expected_result, + module.BackendSupported(manager) + ) + + def __test_get_nodes(self): + module = ExternalTablesModule('something') + module.generate_browser_collection_node = Mock() + + result = module.get_nodes(**self.function_parameters) + six.next(result) + + module.generate_browser_collection_node.assert_called_with( + self.expected_generate_browser_collection_node_called_with + ) + + def __test_template_javascript(self): + module = ExternalTablesModule('something') + self.assertEquals( + self.expected_result, + module.module_use_template_javascript) diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_external_tables_view.py b/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_external_tables_view.py new file mode 100644 index 00000000000..acedc61bf1a --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_external_tables_view.py @@ -0,0 +1,428 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import sys + +from pgadmin.browser.server_groups.servers.databases.external_tables import \ + ExternalTablesView +from pgadmin.utils.route import BaseTestGenerator + +if sys.version_info < (3, 3): + from mock import MagicMock, patch +else: + from unittest.mock import MagicMock, patch + + +class TestExternalTablesView(BaseTestGenerator): + scenarios = [ + ('#check_precondition When executing any http call, ' + 'it saves stores the connection and the manager in the class object', + dict( + test_type='check-precondition', + function_parameters=dict( + server_group_id=0, + server_id=1, + database_id=2, + ), + manager=MagicMock(), + connection=MagicMock(execute_2darray=MagicMock()), + execute_2darray_return_value=(True, dict(rows=[])), + expected_manager_connection_to_be_called_with=dict( + did=2 + ), + )), + ('#nodes When retrieving the children of external tables, ' + 'it return no child ' + 'and status 200', + dict( + test_type='children', + function_parameters=dict( + server_group_id=0, + server_id=1, + database_id=2, + ), + manager=MagicMock(server_type='gpdb', sversion=80323), + connection=MagicMock(execute_2darray=MagicMock()), + execute_2darray_return_value=(True, dict(rows=[])), + + expected_make_json_response_called_with=dict(data=[]), + )), + ('#nodes When retrieving the nodes ' + 'and the database does not have external tables, ' + 'it return no child nodes ' + 'and status 200', + dict( + test_type='nodes', + function_parameters=dict( + server_group_id=0, + server_id=1, + database_id=2, + ), + manager=MagicMock(server_type='gpdb', sversion=80323), + connection=MagicMock(execute_2darray=MagicMock()), + execute_2darray_return_value=(True, dict(rows=[])), + + expect_render_template_called_with='sql/#gpdb#80323#/list.sql', + expected_make_json_response_called_with=dict( + data=[], + status=200 + ), + )), + ('#nodes When retrieving the nodes ' + 'and an error happens while executing the query, ' + 'it return an internal server error ' + 'and status 500', + dict( + test_type='nodes', + function_parameters=dict( + server_group_id=0, + server_id=1, + database_id=2, + ), + + manager=MagicMock(server_type='gpdb', sversion=80323), + connection=MagicMock(execute_2darray=MagicMock()), + execute_2darray_return_value=(False, 'Some error message'), + + expect_render_template_called_with='sql/#gpdb#80323#/list.sql', + expected_internal_server_error_called_with=dict( + errormsg='Some error message' + ), + )), + ('#nodes When retrieving the nodes ' + 'and the database has 2 external tables, ' + 'it return 2 child nodes ' + 'and status 200', + dict( + test_type='nodes', + function_parameters=dict( + server_group_id=0, + server_id=1, + database_id=2, + ), + + manager=MagicMock(server_type='gpdb', sversion=80323), + connection=MagicMock(execute_2darray=MagicMock()), + execute_2darray_return_value=(True, dict( + rows=[ + dict( + oid='oid1', + name='table_one' + ), + dict( + oid='oid2', + name='table_two' + ), + ] + )), + + expect_render_template_called_with='sql/#gpdb#80323#/list.sql', + expected_make_json_response_called_with=dict( + data=[ + { + 'id': "external_table/oid1", + 'label': 'table_one', + 'icon': 'icon-external_table', + 'inode': False, + '_type': 'external_table', + '_id': 'oid1', + '_pid': 2, + 'module': 'pgadmin.node.external_table' + }, + { + 'id': "external_table/oid2", + 'label': 'table_two', + 'icon': 'icon-external_table', + 'inode': False, + '_type': 'external_table', + '_id': 'oid2', + '_pid': 2, + 'module': 'pgadmin.node.external_table' + } + ], + status=200 + ), + )), + ('#node When retrieving the information about 1 external table ' + 'and an error happens while executing the query, ' + 'it return an internal server error ' + 'and status 500', + dict( + test_type='node', + function_parameters=dict( + server_group_id=0, + server_id=1, + database_id=2, + external_table_id=11 + ), + + manager=MagicMock(server_type='gpdb', sversion=80323), + connection=MagicMock(execute_2darray=MagicMock()), + execute_2darray_return_value=(False, 'Some error message'), + + expect_render_template_called_with=dict( + template_name_or_list='sql/#gpdb#80323#/node.sql', + external_table_id=11 + ), + expected_internal_server_error_called_with=dict( + errormsg='Some error message' + ), + )), + ('#node When retrieving the information about 1 external table ' + 'and table does not exist, ' + 'it return an error message ' + 'and status 404', + dict( + test_type='node', + function_parameters=dict( + server_group_id=0, + server_id=1, + database_id=2, + external_table_id=11 + ), + + manager=MagicMock(server_type='gpdb', sversion=80323), + connection=MagicMock(execute_2darray=MagicMock()), + execute_2darray_return_value=(True, dict(rows=[])), + + expect_render_template_called_with=dict( + template_name_or_list='sql/#gpdb#80323#/node.sql', + external_table_id=11 + ), + expected_make_json_response_called_with=dict( + data='Could not find the external table.', + status=404 + ), + )), + ('#nodes When retrieving the information about 1 external table ' + 'and the table exists, ' + 'it return external node information ' + 'and status 200', + dict( + test_type='node', + function_parameters=dict( + server_group_id=0, + server_id=1, + database_id=2, + external_table_id=11 + ), + + manager=MagicMock(server_type='gpdb', sversion=80323), + connection=MagicMock(execute_2darray=MagicMock()), + execute_2darray_return_value=(True, dict( + rows=[ + dict( + oid='oid1', + name='table_one' + ), + dict( + oid='oid2', + name='table_two' + ), + ] + )), + + expect_render_template_called_with=dict( + template_name_or_list='sql/#gpdb#80323#/node.sql', + external_table_id=11 + ), + expected_make_json_response_called_with=dict( + data={ + 'id': "external_table/oid1", + 'label': 'table_one', + 'icon': 'icon-external_table', + 'inode': False, + '_type': 'external_table', + '_id': 'oid1', + '_pid': 2, + 'module': 'pgadmin.node.external_table' + }, + status=200 + ), + )), + ('#properties When retrieving the properties of a external table ' + 'and the table exists, ' + 'it return the properties ' + 'and status 200', + dict( + test_type='properties', + function_parameters=dict( + server_group_id=0, + server_id=1, + database_id=2, + external_table_id=11 + ), + + manager=MagicMock(server_type='gpdb', sversion=80323), + connection=MagicMock(execute_2darray=MagicMock()), + execute_2darray_return_value=(True, dict( + rows=[dict( + urilocation='{http://someurl.com}', + execlocation=['ALL_SEGMENTS'], + fmttype='a', + fmtopts='delimiter \',\' null \'\' ' + 'escape \'"\' quote \'"\'', + command=None, + rejectlimit=None, + rejectlimittype=None, + errtblname=None, + errortofile=None, + pg_encoding_to_char='UTF8', + writable=False, + options=None, + distribution=None, + name='some_table', + namespace='public' + )] + )), + + expect_render_template_called_with=dict( + template_name_or_list='sql/#gpdb#80323#/' + 'get_table_information.sql', + table_oid=11 + ), + expected_make_response_called_with=dict( + response=dict( + name="some_table", + type='readable', + format_type='UTF8', + format_options='delimiter \',\' null \'\' ' + 'escape \'"\' quote \'"\'', + external_options=None, + command=None, + execute_on='all segments', + ), + status=200 + ), + )), + ] + + @patch('pgadmin.browser.server_groups.servers.databases.external_tables' + '.get_driver') + def runTest(self, get_driver_mock): + self.__before_all(get_driver_mock) + + if self.test_type == 'check-precondition': + self.__test_backend_support() + elif self.test_type == 'nodes': + self.__test_nodes() + elif self.test_type == 'node': + self.__test_node() + elif self.test_type == 'children': + self.__test_children() + elif self.test_type == 'properties': + self.__test_properties() + + @patch('pgadmin.browser.server_groups.servers.databases.external_tables' + '.make_json_response') + def __test_children(self, make_json_response_mock): + self.manager.connection = MagicMock(return_value=self.connection) + external_tables_view = ExternalTablesView(cmd='') + external_tables_view.children(**self.function_parameters) + make_json_response_mock.assert_called_with( + **self.expected_make_json_response_called_with + ) + + @patch('pgadmin.browser.server_groups.servers.databases.external_tables' + '.render_template') + def __test_backend_support(self, _): + self.manager.connection = MagicMock(return_value=self.connection) + external_tables_view = ExternalTablesView(cmd='') + external_tables_view.nodes(**self.function_parameters) + self.manager.connection.assert_called_with( + **self.expected_manager_connection_to_be_called_with + ) + self.assertEquals(self.manager, external_tables_view.manager) + self.assertEquals(self.connection, external_tables_view.connection) + + @patch('pgadmin.browser.server_groups.servers.databases.external_tables' + '.render_template') + @patch('pgadmin.browser.server_groups.servers.databases.external_tables' + '.make_json_response') + @patch('pgadmin.browser.server_groups.servers.databases.external_tables' + '.internal_server_error') + def __test_nodes(self, internal_server_error_mock, + make_json_response_mock, render_template_mock): + external_tables_view = ExternalTablesView(cmd='') + external_tables_view.nodes(**self.function_parameters) + if hasattr(self, 'expected_internal_server_error_called_with'): + internal_server_error_mock.assert_called_with( + **self.expected_internal_server_error_called_with + ) + else: + internal_server_error_mock.assert_not_called() + if hasattr(self, 'expected_make_json_response_called_with'): + make_json_response_mock.assert_called_with( + **self.expected_make_json_response_called_with + ) + else: + make_json_response_mock.assert_not_called() + render_template_mock.assert_called_with( + self.expect_render_template_called_with + ) + + @patch('pgadmin.browser.server_groups.servers.databases.external_tables' + '.render_template') + @patch('pgadmin.browser.server_groups.servers.databases.external_tables' + '.make_json_response') + @patch('pgadmin.browser.server_groups.servers.databases.external_tables' + '.internal_server_error') + def __test_node(self, internal_server_error_mock, + make_json_response_mock, render_template_mock): + external_tables_view = ExternalTablesView(cmd='') + external_tables_view.node(**self.function_parameters) + if hasattr(self, 'expected_internal_server_error_called_with'): + internal_server_error_mock.assert_called_with( + **self.expected_internal_server_error_called_with + ) + else: + internal_server_error_mock.assert_not_called() + if hasattr(self, 'expected_make_json_response_called_with'): + make_json_response_mock.assert_called_with( + **self.expected_make_json_response_called_with + ) + else: + make_json_response_mock.assert_not_called() + render_template_mock.assert_called_with( + **self.expect_render_template_called_with + ) + + @patch('pgadmin.browser.server_groups.servers.databases.external_tables' + '.render_template') + @patch('pgadmin.browser.server_groups.servers.databases.external_tables' + '.make_response') + @patch('pgadmin.browser.server_groups.servers.databases.external_tables' + '.internal_server_error') + def __test_properties(self, internal_server_error_mock, + make_response_mock, render_template_mock): + external_tables_view = ExternalTablesView(cmd='') + external_tables_view.properties(**self.function_parameters) + if hasattr(self, 'expected_internal_server_error_called_with'): + internal_server_error_mock.assert_called_with( + **self.expected_internal_server_error_called_with + ) + else: + internal_server_error_mock.assert_not_called() + if hasattr(self, 'expected_make_response_called_with'): + make_response_mock.assert_called_with( + **self.expected_make_response_called_with + ) + else: + make_response_mock.assert_not_called() + render_template_mock.assert_called_with( + **self.expect_render_template_called_with + ) + + def __before_all(self, get_driver_mock): + self.connection.execute_2darray.return_value = \ + self.execute_2darray_return_value + self.manager.connection = MagicMock(return_value=self.connection) + get_driver_mock.return_value = MagicMock( + connection_manager=MagicMock(return_value=self.manager) + ) diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_mapping_utils.py b/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_mapping_utils.py new file mode 100644 index 00000000000..f09b3ff0b78 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_mapping_utils.py @@ -0,0 +1,375 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## +from pgadmin.browser.server_groups.servers.databases \ + .external_tables.mapping_utils import \ + map_column_from_database, map_table_information_from_database, \ + is_web_table, format_options, map_execution_location, map_format_type +from pgadmin.utils.route import BaseTestGenerator + + +class TestMappingUtils(BaseTestGenerator): + scenarios = [ + ('#map_column_from_database When retrieving columns from table, ' + 'it returns only the name and type', + dict( + test_type='map_column_from_database', + function_arguments=dict(column_information=dict( + name='some name', + cltype='some type', + other_column='some other column' + )), + expected_result=dict(name='some name', type='some type') + )), + + ('#map_table_information_from_database When retrieving information ' + 'from web table, ' + 'it returns all fields', + dict( + test_type='map_table_information_from_database', + function_arguments=dict(table_information=dict( + urilocation='{http://someurl.com}', + execlocation=['ALL_SEGMENTS'], + fmttype='b', + fmtopts='delimiter \',\' null \'\' escape \'"\' quote \'"\'', + command=None, + rejectlimit=None, + rejectlimittype=None, + errtblname=None, + errortofile=None, + pg_encoding_to_char='UTF8', + writable=False, + options=None, + distribution=None, + name='some_table_name', + namespace='some_name_space' + )), + expected_result=dict( + uris=['http://someurl.com'], + isWeb=True, + executionLocation=dict(type='all_segments', value=None), + formatType='custom', + formatOptions='delimiter = $$,$$,escape = $$"$$,' + 'null = $$$$,quote = $$"$$', + command=None, + rejectLimit=None, + rejectLimitType=None, + errorTableName=None, + erroToFile=None, + pgEncodingToChar='UTF8', + writable=False, + options=None, + distribution=None, + name='some_table_name', + namespace='some_name_space' + ) + )), + ('#map_table_information_from_database When retrieving information ' + 'from a web table using command instead of URIs, ' + 'it returns all fields', + dict( + test_type='map_table_information_from_database', + function_arguments=dict(table_information=dict( + urilocation=None, + execlocation=['ALL_SEGMENTS'], + fmttype='b', + fmtopts='delimiter \',\' null \'\' escape \'"\' quote \'"\'', + command='cat /tmp/places || echo \'error\'', + rejectlimit=None, + rejectlimittype=None, + errtblname=None, + errortofile=None, + pg_encoding_to_char='UTF8', + writable=False, + options=None, + distribution=None, + name='some_table_name', + namespace='some_name_space' + )), + expected_result=dict( + uris=None, + isWeb=True, + executionLocation=dict(type='all_segments', value=None), + formatType='custom', + formatOptions='delimiter = $$,$$,escape = $$"$$,' + 'null = $$$$,quote = $$"$$', + command='cat /tmp/places || echo \'error\'', + rejectLimit=None, + rejectLimitType=None, + errorTableName=None, + erroToFile=None, + pgEncodingToChar='UTF8', + writable=False, + options=None, + distribution=None, + name='some_table_name', + namespace='some_name_space' + ) + )), + ('#map_table_information_from_database When retrieving information ' + 'from a none web table, ' + 'it returns all fields', + dict( + test_type='map_table_information_from_database', + function_arguments=dict(table_information=dict( + urilocation='{gpfdist://filehost:8081/*.csv}', + execlocation=['ALL_SEGMENTS'], + fmttype='b', + fmtopts='delimiter \',\' null \'\' escape \'"\' quote \'"\'', + command=None, + rejectlimit=None, + rejectlimittype=None, + errtblname=None, + errortofile=None, + pg_encoding_to_char='UTF8', + writable=False, + options=None, + distribution=None, + name='some_table_name', + namespace='some_name_space' + )), + expected_result=dict( + uris=['gpfdist://filehost:8081/*.csv'], + isWeb=False, + executionLocation=dict(type='all_segments', value=None), + formatType='custom', + formatOptions='delimiter = $$,$$,escape = $$"$$,' + 'null = $$$$,quote = $$"$$', + command=None, + rejectLimit=None, + rejectLimitType=None, + errorTableName=None, + erroToFile=None, + pgEncodingToChar='UTF8', + writable=False, + options=None, + distribution=None, + name='some_table_name', + namespace='some_name_space' + ) + )), + + + ('#is_web_table When url starts with http ' + 'and command is None ' + 'it returns true', + dict( + test_type='is_web_table', + function_arguments=dict( + uris='{http://someurl.com}', + command=None + ), + expected_result=True + )), + ('#is_web_table When url starts with https ' + 'and command is None, ' + 'it returns true', + dict( + test_type='is_web_table', + function_arguments=dict( + uris='{https://someurl.com}', + command=None + ), + expected_result=True + )), + ('#is_web_table When url starts with s3 ' + 'and command is None' + 'it returns false', + dict( + test_type='is_web_table', + function_arguments=dict(uris='{s3://someurl.com}', command=None), + expected_result=False + )), + ('#is_web_table When url is None ' + 'and command is not None' + 'it returns false', + dict( + test_type='is_web_table', + function_arguments=dict(uris=None, command='Some command'), + expected_result=True + )), + + + ('#map_execution_location When value is "HOST: 1.1.1.1", ' + 'it returns {type: "host", value: "1.1.1.1"}', + dict( + test_type='map_execution_location', + function_arguments=dict(execution_location=['HOST: 1.1.1.1']), + expected_result=dict(type='host', value='1.1.1.1') + )), + ('#map_execution_location When value is "PER_HOST", ' + 'it returns {type: "per_host", value: None}', + dict( + test_type='map_execution_location', + function_arguments=dict(execution_location=['PER_HOST']), + expected_result=dict(type='per_host', value=None) + )), + ('#map_execution_location When value is "MASTER_ONLY", ' + 'it returns {type: "master_only", value: None}', + dict( + test_type='map_execution_location', + function_arguments=dict(execution_location=['MASTER_ONLY']), + expected_result=dict(type='master_only', value=None) + )), + ('#map_execution_location When value is "SEGMENT_ID: 1234", ' + 'it returns {type: "segment", value: "1234"}', + dict( + test_type='map_execution_location', + function_arguments=dict(execution_location=['SEGMENT_ID: 1234']), + expected_result=dict(type='segment', value='1234') + )), + ('#map_execution_location When value is "TOTAL_SEGS: 4", ' + 'it returns {type: "segments", value: "4"}', + dict( + test_type='map_execution_location', + function_arguments=dict(execution_location=['TOTAL_SEGS: 4']), + expected_result=dict(type='segments', value='4') + )), + ('#map_execution_location When value is "{ALL_SEGMENTS}", ' + 'it returns {type: "all_segments", value: None}', + dict( + test_type='map_execution_location', + function_arguments=dict(execution_location=['ALL_SEGMENTS']), + expected_result=dict(type='all_segments', value=None) + )), + + ('#map_format_type When value is "c", ' + 'it returns csv', + dict( + test_type='map_format_type', + function_arguments=dict(format_type='c'), + expected_result='csv' + )), + ('#map_format_type When value is "something strange", ' + 'it returns csv', + dict( + test_type='map_format_type', + function_arguments=dict(format_type='something strange'), + expected_result='csv' + )), + ('#map_format_type When value is "b", ' + 'it returns custom', + dict( + test_type='map_format_type', + function_arguments=dict(format_type='b'), + expected_result='custom' + )), + ('#map_format_type When value is "t", ' + 'it returns text', + dict( + test_type='map_format_type', + function_arguments=dict(format_type='t'), + expected_result='text' + )), + ('#map_format_type When value is "a", ' + 'it returns avro', + dict( + test_type='map_format_type', + function_arguments=dict(format_type='a'), + expected_result='avro' + )), + ('#map_format_type When value is "p", ' + 'it returns parquet', + dict( + test_type='map_format_type', + function_arguments=dict(format_type='p'), + expected_result='parquet' + )), + + ('#format_options passing None, ' + 'it returns None', + dict( + test_type='format_options', + function_arguments=dict(format_type='avro', options=None), + expected_result=None + )), + ('#format_options passing empty string, ' + 'it returns empty string', + dict( + test_type='format_options', + function_arguments=dict(format_type='parquet', options=''), + expected_result='' + )), + ('#format_options passing "formatter \'fixedwidth_in\' null \' \'", ' + 'it returns "formatter = $$fixedwidth_in$$,null = $$ $$"', + dict( + test_type='format_options', + function_arguments=dict(format_type='custom', + options='formatter \'fixedwidth_in\' ' + 'null \' \''), + expected_result='formatter = $$fixedwidth_in$$,null = $$ $$' + )), + ('#format_options passing ' + '"formatter \'fixedwidth_in\' comma \'\'\' null \' \'", ' + 'it returns ' + '"formatter = $$fixedwidth_in$$,comma = $$\'$$,null = $$ $$"', + dict( + test_type='format_options', + function_arguments=dict(format_type='custom', + options='formatter \'fixedwidth_in\' ' + 'comma \'\'\' null \' \''), + expected_result='comma = $$\'$$,formatter = $$fixedwidth_in$$,' + 'null = $$ $$' + )), + ('#format_options passing ' + '"formatter \'fixedwidth_in\' null \' \' preserve_blanks ' + '\'on\' comma \'\\\'\'", ' + 'it returns ' + '"formatter = $$fixedwidth_in$$,null = $$ $$,preserve_blanks = ' + '$$on$$,comma = $$\'$$"', + dict( + test_type='format_options', + function_arguments=dict(format_type='custom', + options='formatter \'fixedwidth_in\' ' + 'null \' \' ' + 'preserve_blanks \'on\' ' + 'comma \'\'\''), + expected_result='comma = $$\'$$,formatter = $$fixedwidth_in$$,' + 'null = $$ $$,' + 'preserve_blanks = $$on$$' + )), + ('#format_options When format type is text ' + 'it returns escaped string', + dict( + test_type='format_options', + function_arguments=dict(format_type='text', + options='something \'strange\' ' + 'other \'\'\''), + expected_result='other $$\'$$ ' + 'something $$strange$$' + + )), + ('#format_options When format type is csv ' + 'it returns escaped string', + dict( + test_type='format_options', + function_arguments=dict(format_type='csv', + options='something \'strange\' ' + 'other \'\'\''), + expected_result='other $$\'$$ ' + 'something $$strange$$' + + )) + ] + + def runTest(self): + result = None + if self.test_type == 'map_column_from_database': + result = map_column_from_database(**self.function_arguments) + elif self.test_type == 'map_table_information_from_database': + result = map_table_information_from_database( + **self.function_arguments) + elif self.test_type == 'map_execution_location': + result = map_execution_location(**self.function_arguments) + elif self.test_type == 'map_format_type': + result = map_format_type(**self.function_arguments) + elif self.test_type == 'is_web_table': + result = is_web_table(**self.function_arguments) + elif self.test_type == 'format_options': + result = format_options(**self.function_arguments) + self.assertEqual(result, self.expected_result) diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_properties.py b/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_properties.py new file mode 100644 index 00000000000..718151b53a2 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_properties.py @@ -0,0 +1,156 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## +import sys + +from pgadmin.browser.server_groups.servers.databases \ + .external_tables import Properties +from pgadmin.browser.server_groups.servers.databases.external_tables \ + .properties import PropertiesException, PropertiesTableNotFoundException +from pgadmin.utils.route import BaseTestGenerator + +if sys.version_info < (3, 3): + from mock import MagicMock, patch +else: + from unittest.mock import MagicMock, patch + + +class TestExternalTablesView(BaseTestGenerator): + scenarios = [ + ('#properties When retrieving the properties of a external table ' + 'and the table exists, ' + 'it return the properties ', + dict( + test_type='properties', + function_parameters=dict( + table_oid=11 + ), + + connection=MagicMock(execute_2darray=MagicMock()), + execute_2darray_return_value=(True, dict( + rows=[dict( + urilocation='{http://someurl.com}', + execlocation=['ALL_SEGMENTS'], + fmttype='a', + fmtopts='delimiter \',\' null \'\' ' + 'escape \'"\' quote \'"\'', + command=None, + rejectlimit=None, + rejectlimittype=None, + errtblname=None, + errortofile=None, + pg_encoding_to_char='UTF8', + writable=False, + options=None, + distribution=None, + name='some_table', + namespace='public' + )] + )), + + expect_render_template_called_with=dict( + template_name_or_list='some/sql/location/' + 'get_table_information.sql', + table_oid=11 + ), + expected_result=dict( + name="some_table", + type='readable', + format_type='UTF8', + format_options='delimiter \',\' null \'\' ' + 'escape \'"\' quote \'"\'', + external_options=None, + command=None, + execute_on='all segments', + ), + )), + ('#properties When retrieving the properties of a external table ' + 'and a SQL error happens, ' + 'it raises exception with the error message', + dict( + test_type='properties', + function_parameters=dict( + table_oid=11 + ), + + connection=MagicMock(execute_2darray=MagicMock()), + execute_2darray_return_value=(False, 'Some error'), + + expect_render_template_called_with=dict( + template_name_or_list='some/sql/location/' + 'get_table_information.sql', + table_oid=11 + ), + expected_raise_exception=PropertiesException, + expected_internal_server_error_called_with=['Some error'] + )), + ('#properties When retrieving the properties of a external table ' + 'and table is not found, ' + 'it raises exception ', + dict( + test_type='properties', + function_parameters=dict( + table_oid=11 + ), + + connection=MagicMock(execute_2darray=MagicMock()), + execute_2darray_return_value=(True, dict(rows=[])), + + expect_render_template_called_with=dict( + template_name_or_list='some/sql/location/' + 'get_table_information.sql', + table_oid=11 + ), + expected_raise_exception=PropertiesTableNotFoundException + )), + ] + + def runTest(self): + self.connection.execute_2darray.return_value = \ + self.execute_2darray_return_value + self.__test_properties() + + @patch('pgadmin.browser.server_groups.servers.databases' + '.external_tables.properties.internal_server_error') + def __test_properties(self, internal_server_error_mock): + self.maxDiff = None + render_template_mock = MagicMock() + + external_tables_view = Properties( + render_template_mock, + self.connection, + 'some/sql/location/' + ) + + result = None + + try: + result = external_tables_view.retrieve(**self.function_parameters) + if hasattr(self, 'expected_raise_exception'): + self.fail('No exception was raised') + except PropertiesException as exception: + if hasattr(self, 'expected_raise_exception'): + if type(exception) is self.expected_raise_exception: + if hasattr(self, + 'expected_internal_server_error_called_with'): + internal_server_error_mock.assert_called_with( + *self.expected_internal_server_error_called_with + ) + else: + internal_server_error_mock.assert_not_called() + else: + self.fail('Wrong exception type: ' + str(exception)) + else: + raise exception + + if hasattr(self, 'expected_result'): + self.assertEqual(result, self.expected_result) + + render_template_mock.assert_called_with( + **self.expect_render_template_called_with + ) diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_reverse_engineer_ddl.py b/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_reverse_engineer_ddl.py new file mode 100644 index 00000000000..b09040f6e6c --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_reverse_engineer_ddl.py @@ -0,0 +1,261 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import sys + +from pgadmin.browser.server_groups.servers.databases \ + .external_tables.reverse_engineer_ddl import \ + ReverseEngineerDDL, ReverseEngineerDDLException +from pgadmin.utils.route import BaseTestGenerator + +if sys.version_info < (3, 3): + from mock import MagicMock +else: + from unittest.mock import MagicMock + + +class TestReverseEngineerDDL(BaseTestGenerator): + scenarios = [ + ('#execute When retriving the DDL for the creation of external ' + 'tables, ' + 'it retrieves information of the columns and the tables ' + 'and generate the SQL to create the table', + dict( + test_type='execute', + function_parameters=dict(table_oid=14), + find_columns_return_value=dict(somevalue='value'), + table_information_return_value=dict(someother='bamm'), + + expect_find_columns_called_with=14, + expect_table_information_called_with=14, + expect_render_template_called_with=dict( + template_name_or_list='sql/#gpdb#80323#/create.sql', + table=dict( + someother='bamm', + columns=dict(somevalue='value') + ) + ) + )), + ('#find_columns When an external table exists, ' + 'and have 3 columns, ' + 'it returns a list with 1 object that as the table name to inherit ' + 'from', + dict( + test_type='find_columns', + function_parameters={'table_oid': 123}, + execute_2darray_return_value=(True, dict(rows=[ + { + 'name': 'column_1', + 'cltype': 'text', + 'inheritedFrom': 'other_table', + 'inheritedid': '1234', + }, { + 'name': 'column_2', + 'cltype': 'int', + 'inheritedFrom': 'other_table', + 'inheritedid': '1234', + }, { + 'name': 'column_3', + 'cltype': 'numeric', + 'inheritedFrom': 'other_table', + 'inheritedid': '1234', + } + ])), + + expect_render_template_called_with=dict( + template_name_or_list='sql/#gpdb#80323#/get_columns.sql', + table_oid=123 + ), + expected_result=[ + { + 'name': 'column_1', + 'type': 'text' + }, + { + 'name': 'column_2', + 'type': 'int' + }, + { + 'name': 'column_3', + 'type': 'numeric' + }, + ], + )), + ('#find_columns When error happens while retrieving ' + 'column information, ' + 'it raise an exception', + dict( + test_type='find_columns', + function_parameters={'table_oid': 123}, + execute_2darray_return_value=(False, 'Some error message'), + + expect_render_template_called_with=dict( + template_name_or_list='sql/#gpdb#80323#/get_columns.sql', + table_oid=123 + ), + expected_exception=ReverseEngineerDDLException( + 'Some error message'), + ) + ), + ('#table_information When error happens while retrieving ' + 'table generic information, ' + 'it raise an exception', + dict( + test_type='table_information', + function_parameters={'table_oid': 123}, + execute_2darray_return_value=(False, 'Some error message'), + + expect_render_template_called_with=dict( + template_name_or_list='sql/#gpdb#80323#/' + 'get_table_information.sql', + table_oid=123 + ), + expected_exception=ReverseEngineerDDLException( + 'Some error message'), + ) + ), + ('#table_information When cannot find the table, ' + 'it raise an exception', + dict( + test_type='table_information', + function_parameters={'table_oid': 123}, + execute_2darray_return_value=(True, {'rows': []}), + + expect_render_template_called_with=dict( + template_name_or_list='sql/#gpdb#80323#/' + 'get_table_information.sql', + table_oid=123 + ), + expected_exception=ReverseEngineerDDLException( + 'Table not found'), + )), + ('#table_information When retrieving generic information ' + 'about a Web table, ' + 'it returns the table information', + dict( + test_type='table_information', + function_parameters={'table_oid': 123}, + execute_2darray_return_value=(True, dict(rows=[ + { + 'urilocation': '{http://someurl.com}', + 'execlocation': ['ALL_SEGMENTS'], + 'fmttype': 'a', + 'fmtopts': 'delimiter \',\' null \'\' ' + 'escape \'"\' quote \'"\'', + 'command': None, + 'rejectlimit': None, + 'rejectlimittype': None, + 'errtblname': None, + 'errortofile': None, + 'pg_encoding_to_char': 'UTF8', + 'writable': False, + 'options': None, + 'distribution': None, + 'name': 'some_table', + 'namespace': 'public' + } + ])), + + expect_render_template_called_with=dict( + template_name_or_list='sql/#gpdb#80323#/' + 'get_table_information.sql', + table_oid=123 + ), + expected_result={ + 'uris': ['http://someurl.com'], + 'isWeb': True, + 'executionLocation': dict(type='all_segments', value=None), + 'formatType': 'avro', + 'formatOptions': 'delimiter = $$,$$,escape = $$"$$,' + 'null = $$$$,quote = $$"$$', + 'command': None, + 'rejectLimit': None, + 'rejectLimitType': None, + 'errorTableName': None, + 'erroToFile': None, + 'pgEncodingToChar': 'UTF8', + 'writable': False, + 'options': None, + 'distribution': None, + 'name': 'some_table', + 'namespace': 'public' + }, + )), + ] + + def __init__(self, *args, **kwargs): + super(TestReverseEngineerDDL, self).__init__(*args, **kwargs) + self.connection = None + self.subject = None + self.render_template_mock = None + + def runTest(self): + self.render_template_mock = MagicMock() + self.connection = MagicMock(execute_2darray=MagicMock()) + if hasattr(self, 'execute_2darray_return_value'): + self.connection.execute_2darray.return_value = \ + self.execute_2darray_return_value + self.subject = ReverseEngineerDDL( + 'sql/#gpdb#80323#/', + self.render_template_mock, + self.connection, + 1, 2, 3) + if self.test_type == 'find_columns': + self.__test_find_columns() + elif self.test_type == 'table_information': + self.__test_table_information() + elif self.test_type == 'execute': + self.__test_execute() + + def __test_find_columns(self): + if hasattr(self, 'expected_exception'): + try: + self.subject.find_columns(**self.function_parameters) + self.fail('Exception not raise') + except ReverseEngineerDDLException as exception: + self.assertEqual(str(exception), + str(self.expected_exception)) + else: + result = self.subject.find_columns(**self.function_parameters) + self.assertEqual(self.expected_result, result) + + self.render_template_mock.assert_called_with( + **self.expect_render_template_called_with + ) + + def __test_table_information(self): + if hasattr(self, 'expected_exception'): + try: + self.subject.table_information(**self.function_parameters) + self.fail('Exception not raise') + except ReverseEngineerDDLException as exception: + self.assertEqual(str(exception), + str(self.expected_exception)) + else: + result = self.subject.table_information(**self.function_parameters) + self.assertEqual(self.expected_result, result) + + self.render_template_mock.assert_called_with( + **self.expect_render_template_called_with + ) + + def __test_execute(self): + self.subject.find_columns = MagicMock( + return_value=self.find_columns_return_value) + self.subject.table_information = MagicMock( + return_value=self.table_information_return_value) + + self.subject.execute(**self.function_parameters) + + self.subject.find_columns.assert_called_with( + self.expect_find_columns_called_with) + self.subject.table_information.assert_called_with( + self.expect_table_information_called_with) + self.render_template_mock.assert_called_with( + **self.expect_render_template_called_with) diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_sql_template_create_integration.py b/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_sql_template_create_integration.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/table/sql/gpdb_5.0_plus/nodes.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/table/sql/gpdb_5.0_plus/nodes.sql index b0bc8423770..3e877bfce97 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/table/sql/gpdb_5.0_plus/nodes.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/table/sql/gpdb_5.0_plus/nodes.sql @@ -4,6 +4,7 @@ SELECT rel.oid, rel.relname AS name, FROM pg_class rel WHERE rel.relkind IN ('r','s','t') AND rel.relnamespace = {{ scid }}::oid AND rel.relname NOT IN (SELECT partitiontablename FROM pg_partitions) + AND rel.oid NOT IN (SELECT reloid from pg_exttable) {% if tid %} AND rel.oid = {{tid}}::OID {% endif %} diff --git a/web/pgadmin/static/bundle/browser.js b/web/pgadmin/static/bundle/browser.js index d39ce22b208..3fcc69d8083 100644 --- a/web/pgadmin/static/bundle/browser.js +++ b/web/pgadmin/static/bundle/browser.js @@ -1,5 +1,6 @@ define('bundled_browser',[ 'pgadmin.browser', + 'sources/browser/server_groups/servers/databases/external_tables/index', ], function(pgBrowser) { pgBrowser.init(); }); diff --git a/web/pgadmin/static/js/browser/server_groups/servers/databases/external_tables/external_tables.js b/web/pgadmin/static/js/browser/server_groups/servers/databases/external_tables/external_tables.js new file mode 100644 index 00000000000..f564e169f43 --- /dev/null +++ b/web/pgadmin/static/js/browser/server_groups/servers/databases/external_tables/external_tables.js @@ -0,0 +1,88 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2018, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +export function initialize(pgBrowser, gettext) { + if (!pgBrowser.Nodes['coll-external_table']) { + pgBrowser.Nodes['coll-external_table'] = + pgBrowser.Collection.extend({ + node: 'external_table', + label: gettext('External Tables'), + type: 'coll-external_tables', + columns: ['name', 'fdwowner', 'description'], + }); + } + + if (!pgBrowser.Nodes['external_table']) { + pgBrowser.Nodes['external_table'] = pgBrowser.Node.extend({ + parent_type: 'database', + type: 'external_table', + label: gettext('External Table'), + collection_type: 'coll-external_table', + hasSQL: true, + model: pgBrowser.Node.Model.extend({ + defaults: { + name: undefined, + type: undefined, + encoding: undefined, + format_type: undefined, + format_option: undefined, + external_options: undefined, + command: undefined, + execute_on: undefined, + }, + schema: [ + { + id: 'name', + label: gettext('Name'), + type: 'text', + mode: ['properties'], + }, { + id: 'type', + label: gettext('Type'), + type: 'text', + mode: ['properties'], + }, { + id: 'encoding', + label: gettext('Encoding'), + type: 'text', + mode: ['properties'], + }, { + id: 'format_type', + label: gettext('Format Type'), + type: 'text', + mode: ['properties'], + }, { + id: 'format_option', + label: gettext('Format Optionos'), + type: 'text', + mode: ['properties'], + }, { + id: 'external_options', + label: gettext('External Options'), + type: 'text', + mode: ['properties'], + }, { + id: 'command', + label: gettext('Command'), + type: 'text', + mode: ['properties'], + }, { + id: 'execute_on', + label: gettext('Execute on'), + type: 'text', + mode: ['properties'], + }, + ], + }), + }); + } + + return pgBrowser; +} + diff --git a/web/pgadmin/static/js/browser/server_groups/servers/databases/external_tables/index.js b/web/pgadmin/static/js/browser/server_groups/servers/databases/external_tables/index.js new file mode 100644 index 00000000000..54cd14bc0bb --- /dev/null +++ b/web/pgadmin/static/js/browser/server_groups/servers/databases/external_tables/index.js @@ -0,0 +1,18 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2018, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import pgBrowser from 'top/browser/static/js/browser'; +import gettext from 'sources/gettext'; +import {initialize} from './external_tables'; + +let pgBrowserOut = initialize(pgBrowser, gettext); + +module.exports = { + pgBrowser: pgBrowserOut, +}; diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index 0f3c909910c..cc0aa4cf5ee 100644 --- a/web/pgadmin/tools/sqleditor/__init__.py +++ b/web/pgadmin/tools/sqleditor/__init__.py @@ -438,6 +438,7 @@ def poll(trans_id): 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'] diff --git a/web/regression/javascript/browser/server_groups/servers/databases/external_tables/external_tables_spec.js b/web/regression/javascript/browser/server_groups/servers/databases/external_tables/external_tables_spec.js new file mode 100644 index 00000000000..d95e51a5cfe --- /dev/null +++ b/web/regression/javascript/browser/server_groups/servers/databases/external_tables/external_tables_spec.js @@ -0,0 +1,56 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2018, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import {initialize} from 'sources/browser/server_groups/servers/databases/external_tables/external_tables'; + +describe('when external tables is loaded', () => { + let pgBrowser; + let gettext; + let result; + beforeEach(() => { + pgBrowser = { + Nodes: {}, + }; + pgBrowser.Collection = jasmine.createSpyObj('Collection', ['extend']); + pgBrowser.Node = jasmine.createSpyObj('Node', ['extend', 'Model']); + pgBrowser.Node.Model = jasmine.createSpyObj('Model', ['extend']); + pgBrowser.Collection.extend.and.returnValue('extended object'); + pgBrowser.Node.extend.and.returnValue('extended node object'); + gettext = jasmine.createSpy('gettext').and.callFake((text) => text); + }); + + describe('when external tables is already defined', () => { + beforeEach(() => { + pgBrowser.Nodes['coll-external_table'] = {}; + result = initialize(pgBrowser, gettext); + }); + + it('does not reinitialize it', () => { + expect(pgBrowser.Collection.extend).not.toHaveBeenCalled(); + }); + + it('returns the not updated version of pgBrowser', () => { + expect(result).toBe(pgBrowser); + }); + }); + + describe('when external tables is not defined', () => { + beforeEach(() => { + result = initialize(pgBrowser, gettext); + }); + + it('initializes "coll-external_tables"', () => { + expect(pgBrowser.Collection.extend).toHaveBeenCalled(); + }); + + it('returns the updated version of pgBrowser', () => { + expect(result.Nodes['coll-external_table']).not.toBeUndefined(); + }); + }); +}); diff --git a/web/webpack.config.js b/web/webpack.config.js index 84f8466eeb9..4dbf1357893 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -146,12 +146,21 @@ module.exports = { presets: ['es2015', 'react'], }, }, + }, { + test: /external_table.*\.js/, + use: { + loader: 'babel-loader', + options: { + presets: ['es2015'], + }, + }, }, { // Transforms the code in a way that it works in the webpack environment. // It uses imports-loader internally to load dependency. Its // configuration is specified in webpack.shim.js // Ref: https://www.npmjs.com/package/shim-loader test: /\.js/, + exclude: [/external_table/], loader: 'shim-loader', query: webpackShimConfig, include: path.join(__dirname, '/pgadmin/browser'), diff --git a/web/webpack.shim.js b/web/webpack.shim.js index 58b26042e98..1b6442a61df 100644 --- a/web/webpack.shim.js +++ b/web/webpack.shim.js @@ -120,6 +120,7 @@ var webpackShimConfig = { // Map module id to file path used in 'define(['baseurl', 'misc']). It is // used by webpack while creating bundle resolveAlias: { + 'top': path.join(__dirname, './pgadmin'), 'bundled_codemirror': path.join(__dirname, './pgadmin/static/bundle/codemirror'), 'bundled_browser': path.join(__dirname, './pgadmin/static/bundle/browser'), 'sources': path.join(__dirname, './pgadmin/static/js'), @@ -213,6 +214,8 @@ var webpackShimConfig = { 'pgadmin.node.catalog_object': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/catalog_objects/static/js/catalog_object'), 'pgadmin.dashboard': path.join(__dirname, './pgadmin/dashboard/static/js/dashboard'), 'pgadmin.node.foreign_data_wrapper': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/static/js/foreign_data_wrapper'), + 'pgadmin.node.external_table': path.join(__dirname, './pgadmin/static/js/browser/server_groups/servers/databases/external_tables/index'), + 'pgadmin.node.external_tables': path.join(__dirname, './pgadmin/static/js/browser/server_groups/servers/databases/external_tables/index'), 'pgadmin.node.foreign_key': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key'), 'pgadmin.browser.server.variable': path.join(__dirname, './pgadmin/browser/server_groups/servers/static/js/variable'), 'pgadmin.tools.grant_wizard': path.join(__dirname, './pgadmin/tools/grant_wizard/static/js/grant_wizard'),