From b08e90ea88abe90688d89fc1e074d5da4e7d05c6 Mon Sep 17 00:00:00 2001 From: Pravesh Sharma Date: Wed, 23 Apr 2025 15:35:42 +0530 Subject: [PATCH] Add support for type constructors for PostGIS spatial types. #2256 --- .github/workflows/run-python-tests-epas.yml | 6 +- .github/workflows/run-python-tests-pg.yml | 3 +- .../databases/schemas/tables/__init__.py | 18 +++ .../tables/columns/static/js/column.ui.js | 44 +++++- .../databases/schemas/tables/columns/utils.py | 37 +++-- .../templates/tables/sql/11_plus/create.sql | 2 +- .../templates/tables/sql/12_plus/create.sql | 2 +- .../templates/tables/sql/14_plus/create.sql | 2 +- .../templates/tables/sql/16_plus/create.sql | 2 +- .../templates/tables/sql/default/create.sql | 2 +- .../schemas/tables/tests/table_test_data.json | 47 ++++++ .../tables/tests/test_geometry_table.py | 136 ++++++++++++++++++ .../databases/schemas/tables/tests/utils.py | 71 +++++++++ .../servers/databases/schemas/utils.py | 26 ++++ web/pgadmin/tools/erd/__init__.py | 8 +- .../static/js/erd_tool/components/ERDTool.jsx | 3 +- .../erd/static/js/erd_tool/dialogs/index.jsx | 2 +- .../static/js/erd_tool/nodes/TableNode.jsx | 6 +- web/pgadmin/tools/erd/utils.py | 9 ++ web/pgadmin/utils/__init__.py | 13 ++ .../python_test_utils/test_utils.py | 7 + 21 files changed, 426 insertions(+), 20 deletions(-) create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/test_geometry_table.py diff --git a/.github/workflows/run-python-tests-epas.yml b/.github/workflows/run-python-tests-epas.yml index c3953a61f..f89c3249e 100644 --- a/.github/workflows/run-python-tests-epas.yml +++ b/.github/workflows/run-python-tests-epas.yml @@ -48,7 +48,7 @@ jobs: if: ${{ matrix.os == 'ubuntu-22.04' }} run: | sudo apt update - sudo apt install -y libpq-dev libffi-dev libssl-dev libkrb5-dev zlib1g-dev edb-as${{ matrix.pgver }}-server edb-as${{ matrix.pgver }}-server-pldebugger + sudo apt install -y libpq-dev libffi-dev libssl-dev libkrb5-dev zlib1g-dev edb-as${{ matrix.pgver }}-server edb-as${{ matrix.pgver }}-server-pldebugger edb-as${{ matrix.pgver }}-postgis34 - name: Install pgagent on Linux if: ${{ matrix.os == 'ubuntu-22.04' && matrix.pgver <= 16 }} @@ -105,6 +105,10 @@ jobs: - name: Create pgagent extension on Linux if: ${{ matrix.os == 'ubuntu-22.04' && matrix.pgver <= 16 }} run: psql -U enterprisedb -d postgres -p 58${{ matrix.pgver }} -c 'CREATE EXTENSION IF NOT EXISTS pgagent;' + + - name: Create postgis extension on Linux + if: ${{ matrix.os == 'ubuntu-22.04' && matrix.pgver <= 16 }} + run: psql -U enterprisedb -d postgres -p 58${{ matrix.pgver }} -c 'CREATE EXTENSION IF NOT EXISTS postgis;' - name: Install Python dependencies on Linux if: ${{ matrix.os == 'ubuntu-22.04' }} diff --git a/.github/workflows/run-python-tests-pg.yml b/.github/workflows/run-python-tests-pg.yml index 4489a06cc..e58592aee 100644 --- a/.github/workflows/run-python-tests-pg.yml +++ b/.github/workflows/run-python-tests-pg.yml @@ -51,7 +51,7 @@ jobs: if: ${{ matrix.os == 'ubuntu-22.04' }} run: | sudo apt update - sudo apt install -y libpq-dev libffi-dev libssl-dev libkrb5-dev zlib1g-dev postgresql-${{ matrix.pgver }} postgresql-${{ matrix.pgver }}-pldebugger pgagent + sudo apt install -y libpq-dev libffi-dev libssl-dev libkrb5-dev zlib1g-dev postgresql-${{ matrix.pgver }} postgresql-${{ matrix.pgver }}-pldebugger pgagent postgresql-${{ matrix.pgver }}-postgis-3 - name: Install platform dependencies on macOS if: ${{ matrix.os == 'macos-latest' }} @@ -113,6 +113,7 @@ jobs: psql -U postgres -p 59${{ matrix.pgver }} -c 'CREATE EXTENSION pgagent;' psql -U postgres -p 59${{ matrix.pgver }} -c 'CREATE EXTENSION pldbgapi;' + psql -U postgres -p 59${{ matrix.pgver }} -c 'CREATE EXTENSION postgis;' - name: Start PostgreSQL on macOS if: ${{ matrix.os == 'macos-latest' }} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py index 2ba1962e1..a65c91ed6 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/__init__.py @@ -308,6 +308,9 @@ class TableView(BaseTableView, DataTypeReader, SchemaDiffTableCompare): 'count_rows': [{'get': 'count_rows'}], 'compare': [{'get': 'compare'}, {'get': 'compare'}], 'get_op_class': [{'get': 'get_op_class'}, {'get': 'get_op_class'}], + 'get_geometry_types': [ + {'get': 'geometry_types'}, + {'get': 'geometry_types'}], }) @BaseTableView.check_precondition @@ -695,6 +698,21 @@ class TableView(BaseTableView, DataTypeReader, SchemaDiffTableCompare): status=200 ) + @BaseTableView.check_precondition + def geometry_types(self, gid, sid, did, scid, tid=None, clid=None): + """ + Returns: + This function will return list of geometry types available for + column node for node-ajax-control + """ + status, types = self.get_geometry_types(self.conn) + if not status: + return internal_server_error(errormsg=types) + return make_json_response( + data=types, + status=200 + ) + @BaseTableView.check_precondition def get_columns(self, gid, sid, did, scid, tid=None): """ diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui.js index bbf2810bb..4160a352e 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui.js @@ -24,11 +24,14 @@ export function getNodeColumnSchema(treeNodeInfo, itemNodeData, pgBrowser) { cacheLevel: 'table', }), ()=>getNodeAjaxOptions('get_collations', pgBrowser.Nodes['collation'], treeNodeInfo, itemNodeData), + ()=>getNodeAjaxOptions('get_geometry_types', pgBrowser.Nodes['table'], treeNodeInfo, itemNodeData, { + cacheLevel: 'table', + }), ); } export default class ColumnSchema extends BaseUISchema { - constructor(getPrivilegeRoleSchema, nodeInfo, cltypeOptions, collspcnameOptions, inErd=false) { + constructor(getPrivilegeRoleSchema, nodeInfo, cltypeOptions, collspcnameOptions, geometryTypes, inErd=false) { super({ name: undefined, attowner: undefined, @@ -60,12 +63,15 @@ export default class ColumnSchema extends BaseUISchema { seqcycle: undefined, colconstype: 'n', genexpr: undefined, + srid: null, + geometry: null, }); this.getPrivilegeRoleSchema = getPrivilegeRoleSchema; this.nodeInfo = nodeInfo; this.cltypeOptions = cltypeOptions; this.collspcnameOptions = collspcnameOptions; + this.geometryTypes = geometryTypes; this.inErd = inErd; this.datatypes = []; @@ -278,6 +284,42 @@ export default class ColumnSchema extends BaseUISchema { } return false; }, + },{ + id:'geometry', label: gettext('Geometry Type'), deps: ['cltype'], + group: gettext('Definition'), type: 'select', options: this.geometryTypes, + disabled: (state) => { + return !(state.cltype == 'geometry' || state.cltype == 'geography'); + }, + depChange: (state) => { + let cltype = state.cltype; + if (cltype != 'geometry' && cltype != 'geography') { + return { + ...state, + geometry: null + }; + } + }, + visible: (state) => { + return (state.cltype == 'geometry' || state.cltype == 'geography'); + } + },{ + id:'srid', label: gettext('SRID'), deps: ['cltype'], + group: gettext('Definition'), type: 'int', + depChange: (state) => { + let cltype = state.cltype; + if (cltype != 'geometry' && cltype != 'geography') { + return { + ...state, + srid: null + }; + } + }, + disabled: function(state) { + return !(state.cltype == 'geometry' || state.cltype == 'geography'); + }, + visible: function(state) { + return (state.cltype == 'geometry' || state.cltype == 'geography'); + } },{ id: 'attlen', label: gettext('Length/Precision'), deps: ['cltype'], type: 'int', group: gettext('Definition'), width: 120, enableResizing: false, diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/utils.py index 548339d3c..e16ad3625 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/utils.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/utils.py @@ -20,6 +20,7 @@ from pgadmin.browser.server_groups.servers.utils import parse_priv_from_db, \ from pgadmin.browser.server_groups.servers.databases.utils \ import make_object_name from functools import wraps +import re def get_template_path(f): @@ -448,19 +449,24 @@ def fetch_length_precision(data): data['attlen'] = None data['attprecision'] = None - import re + if 'typname' in data and (data['typname'] in ('geometry', 'geography')): + # If we have geometry column + parmas = parse_params(data['cltype']) + if parmas: + data['geometry'] = parmas[0] + data['srid'] = parmas[1] + else: + parmas = parse_params(fulltype) # If we have length & precision both if length and precision: - match_obj = re.search(r'(\d+),(\d+)', fulltype) - if match_obj: - data['attlen'] = match_obj.group(1) - data['attprecision'] = match_obj.group(2) + if parmas: + data['attlen'] = parmas[0] + data['attprecision'] = parmas[1] elif length: # If we have length only - match_obj = re.search(r'(\d+)', fulltype) - if match_obj: - data['attlen'] = match_obj.group(1) + if parmas: + data['attlen'] = parmas[0] data['attprecision'] = None return data @@ -474,3 +480,18 @@ def parse_column_variables(col_variables): k, v = spcoption.split('=') spcoptions.append({'name': k, 'value': v}) return spcoptions + + +def parse_params(fulltype): + """ + This function will fetch length and precision details + from fulltype. + + :param fulltype: Full type. + :param data: Data. + """ + + match_obj = re.search(r'\((\d*[a-zA-Z]*),?(\d*)\)', fulltype) + if match_obj: + return [match_obj.group(1), match_obj.group(2)] + return False diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/11_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/11_plus/create.sql index 6a8ae8d50..5370bdbca 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/11_plus/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/11_plus/create.sql @@ -46,7 +46,7 @@ CREATE {% if data.relpersistence %}UNLOGGED {% endif %}TABLE{% if add_not_exists {% if data.columns and data.columns|length > 0 %} {% for c in data.columns %} {% if c.name and c.cltype %} - {% if c.inheritedfromtable %}-- Inherited from table {{c.inheritedfromtable}}: {% elif c.inheritedfromtype %}-- Inherited from type {{c.inheritedfromtype}}: {% endif %}{{conn|qtIdent(c.name)}} {% if is_sql %}{{c.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, c.cltype, c.attlen, c.attprecision, c.hasSqrBracket) }}{% endif %}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.attnotnull %} NOT NULL{% endif %}{% if c.defval is defined and c.defval is not none and c.defval != '' %} DEFAULT {{c.defval}}{% endif %} + {% if c.inheritedfromtable %}-- Inherited from table {{c.inheritedfromtable}}: {% elif c.inheritedfromtype %}-- Inherited from type {{c.inheritedfromtype}}: {% endif %}{{conn|qtIdent(c.name)}} {% if is_sql %}{{c.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, c.cltype, c.attlen, c.attprecision, c.hasSqrBracket) }}{% endif %}{% if c.geometry and not is_sql %}({{c.geometry}}{% if c.srid %},{{c.srid}}{% endif %}){% endif %}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.attnotnull %} NOT NULL{% endif %}{% if c.defval is defined and c.defval is not none and c.defval != '' %} DEFAULT {{c.defval}}{% endif %} {% if c.colconstype == 'i' and c.attidentity and c.attidentity != '' %} {% if c.attidentity == 'a' %} GENERATED ALWAYS AS IDENTITY{% elif c.attidentity == 'd' %} GENERATED BY DEFAULT AS IDENTITY{% endif %} {% if c.seqincrement or c.seqcycle or c.seqincrement or c.seqstart or c.seqmin or c.seqmax or c.seqcache %} ( {% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/12_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/12_plus/create.sql index 20ed95c45..6abb15cd2 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/12_plus/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/12_plus/create.sql @@ -52,7 +52,7 @@ CREATE {% if data.relpersistence %}UNLOGGED {% endif %}TABLE{% if add_not_exists {% if data.columns and data.columns|length > 0 %} {% for c in data.columns %} {% if c.name and c.cltype %} - {% if c.inheritedfromtable %}-- Inherited from table {{c.inheritedfromtable}}: {% elif c.inheritedfromtype %}-- Inherited from type {{c.inheritedfromtype}}: {% endif %}{{conn|qtIdent(c.name)}} {% if is_sql %}{{c.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, c.cltype, c.attlen, c.attprecision, c.hasSqrBracket) }}{% endif %}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.attnotnull %} NOT NULL{% endif %}{% if c.defval is defined and c.defval is not none and c.defval != '' and c.colconstype != 'g' %} DEFAULT {{c.defval}}{% endif %} + {% if c.inheritedfromtable %}-- Inherited from table {{c.inheritedfromtable}}: {% elif c.inheritedfromtype %}-- Inherited from type {{c.inheritedfromtype}}: {% endif %}{{conn|qtIdent(c.name)}} {% if is_sql %}{{c.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, c.cltype, c.attlen, c.attprecision, c.hasSqrBracket) }}{% endif %}{% if c.geometry and not is_sql %}({{c.geometry}}{% if c.srid %},{{c.srid}}{% endif %}){% endif %}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.attnotnull %} NOT NULL{% endif %}{% if c.defval is defined and c.defval is not none and c.defval != '' and c.colconstype != 'g' %} DEFAULT {{c.defval}}{% endif %} {% if c.colconstype == 'i' and c.attidentity and c.attidentity != '' %} {% if c.attidentity == 'a' %} GENERATED ALWAYS AS IDENTITY{% elif c.attidentity == 'd' %} GENERATED BY DEFAULT AS IDENTITY{% endif %} {% if c.seqincrement or c.seqcycle or c.seqincrement or c.seqstart or c.seqmin or c.seqmax or c.seqcache %} ( {% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/14_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/14_plus/create.sql index 0c60929e0..ff937eed8 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/14_plus/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/14_plus/create.sql @@ -54,7 +54,7 @@ CREATE {% if data.relpersistence %}UNLOGGED {% endif %}TABLE{% if add_not_exists {% if data.columns and data.columns|length > 0 %} {% for c in data.columns %} {% if c.name and c.cltype %} - {% if c.inheritedfromtable %}-- Inherited from table {{c.inheritedfromtable}}: {% elif c.inheritedfromtype %}-- Inherited from type {{c.inheritedfromtype}}: {% endif %}{{conn|qtIdent(c.name)}} {% if is_sql %}{{c.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, c.cltype, c.attlen, c.attprecision, c.hasSqrBracket) }}{% endif %}{% if c.attcompression is defined and c.attcompression is not none and c.attcompression != '' %} COMPRESSION {{c.attcompression}}{% endif %}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.attnotnull %} NOT NULL{% endif %}{% if c.defval is defined and c.defval is not none and c.defval != '' and c.colconstype != 'g' %} DEFAULT {{c.defval}}{% endif %} + {% if c.inheritedfromtable %}-- Inherited from table {{c.inheritedfromtable}}: {% elif c.inheritedfromtype %}-- Inherited from type {{c.inheritedfromtype}}: {% endif %}{{conn|qtIdent(c.name)}} {% if is_sql %}{{c.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, c.cltype, c.attlen, c.attprecision, c.hasSqrBracket) }}{% endif %}{% if c.geometry and not is_sql %}({{c.geometry}}{% if c.srid %},{{c.srid}}{% endif %}){% endif %}{% if c.attcompression is defined and c.attcompression is not none and c.attcompression != '' %} COMPRESSION {{c.attcompression}}{% endif %}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.attnotnull %} NOT NULL{% endif %}{% if c.defval is defined and c.defval is not none and c.defval != '' and c.colconstype != 'g' %} DEFAULT {{c.defval}}{% endif %} {% if c.colconstype == 'i' and c.attidentity and c.attidentity != '' %} {% if c.attidentity == 'a' %} GENERATED ALWAYS AS IDENTITY{% elif c.attidentity == 'd' %} GENERATED BY DEFAULT AS IDENTITY{% endif %} {% if c.seqincrement or c.seqcycle or c.seqincrement or c.seqstart or c.seqmin or c.seqmax or c.seqcache %} ( {% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/16_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/16_plus/create.sql index f95d73230..b12560e75 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/16_plus/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/16_plus/create.sql @@ -54,7 +54,7 @@ CREATE {% if data.relpersistence %}UNLOGGED {% endif %}TABLE{% if add_not_exists {% if data.columns and data.columns|length > 0 %} {% for c in data.columns %} {% if c.name and c.cltype %} - {% if c.inheritedfromtable %}-- Inherited from table {{c.inheritedfromtable}}: {% elif c.inheritedfromtype %}-- Inherited from type {{c.inheritedfromtype}}: {% endif %}{{conn|qtIdent(c.name)}} {% if is_sql %}{{c.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, c.cltype, c.attlen, c.attprecision, c.hasSqrBracket) }}{% endif %}{%if c.attstorage is defined and c.attstorage != c.defaultstorage%} STORAGE {%if c.attstorage == 'p' %}PLAIN{% elif c.attstorage == 'm'%}MAIN{% elif c.attstorage == 'e'%}EXTERNAL{% elif c.attstorage == 'x'%}EXTENDED{% elif c.attstorage == 'd'%}DEFAULT{% endif %}{% endif %}{% if c.attcompression is defined and c.attcompression is not none and c.attcompression != '' %} COMPRESSION {{c.attcompression}}{% endif %}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.attnotnull %} NOT NULL{% endif %}{% if c.defval is defined and c.defval is not none and c.defval != '' and c.colconstype != 'g' %} DEFAULT {{c.defval}}{% endif %} + {% if c.inheritedfromtable %}-- Inherited from table {{c.inheritedfromtable}}: {% elif c.inheritedfromtype %}-- Inherited from type {{c.inheritedfromtype}}: {% endif %}{{conn|qtIdent(c.name)}} {% if is_sql %}{{c.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, c.cltype, c.attlen, c.attprecision, c.hasSqrBracket) }}{% endif %}{% if c.geometry and not is_sql %}({{c.geometry}}{% if c.srid %},{{c.srid}}{% endif %}){% endif %}{%if c.attstorage is defined and c.attstorage != c.defaultstorage%} STORAGE {%if c.attstorage == 'p' %}PLAIN{% elif c.attstorage == 'm'%}MAIN{% elif c.attstorage == 'e'%}EXTERNAL{% elif c.attstorage == 'x'%}EXTENDED{% elif c.attstorage == 'd'%}DEFAULT{% endif %}{% endif %}{% if c.attcompression is defined and c.attcompression is not none and c.attcompression != '' %} COMPRESSION {{c.attcompression}}{% endif %}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.attnotnull %} NOT NULL{% endif %}{% if c.defval is defined and c.defval is not none and c.defval != '' and c.colconstype != 'g' %} DEFAULT {{c.defval}}{% endif %} {% if c.colconstype == 'i' and c.attidentity and c.attidentity != '' %} {% if c.attidentity == 'a' %} GENERATED ALWAYS AS IDENTITY{% elif c.attidentity == 'd' %} GENERATED BY DEFAULT AS IDENTITY{% endif %} {% if c.seqincrement or c.seqcycle or c.seqincrement or c.seqstart or c.seqmin or c.seqmax or c.seqcache %} ( {% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/default/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/default/create.sql index 6b2a3cba1..e1bb0c73f 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/default/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/tables/sql/default/create.sql @@ -46,7 +46,7 @@ CREATE {% if data.relpersistence %}UNLOGGED {% endif %}TABLE{% if add_not_exists {% if data.columns and data.columns|length > 0 %} {% for c in data.columns %} {% if c.name and c.cltype %} - {% if c.inheritedfromtable %}-- Inherited from table {{c.inheritedfromtable}}: {% elif c.inheritedfromtype %}-- Inherited from type {{c.inheritedfromtype}}: {% endif %}{{conn|qtIdent(c.name)}} {% if is_sql %}{{c.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, c.cltype, c.attlen, c.attprecision, c.hasSqrBracket) }}{% endif %}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.attnotnull %} NOT NULL{% endif %}{% if c.defval is defined and c.defval is not none and c.defval != '' %} DEFAULT {{c.defval}}{% endif %} + {% if c.inheritedfromtable %}-- Inherited from table {{c.inheritedfromtable}}: {% elif c.inheritedfromtype %}-- Inherited from type {{c.inheritedfromtype}}: {% endif %}{{conn|qtIdent(c.name)}} {% if is_sql %}{{c.displaytypname}}{% else %}{{ GET_TYPE.CREATE_TYPE_SQL(conn, c.cltype, c.attlen, c.attprecision, c.hasSqrBracket) }}{% endif %}{% if c.geometry and not is_sql %}({{c.geometry}}{% if c.srid %},{{c.srid}}{% endif %}){% endif %}{% if c.collspcname %} COLLATE {{c.collspcname}}{% endif %}{% if c.attnotnull %} NOT NULL{% endif %}{% if c.defval is defined and c.defval is not none and c.defval != '' %} DEFAULT {{c.defval}}{% endif %} {% if c.colconstype == 'i' and c.attidentity and c.attidentity != '' %} {% if c.attidentity == 'a' %} GENERATED ALWAYS AS IDENTITY{% elif c.attidentity == 'd' %} GENERATED BY DEFAULT AS IDENTITY{% endif %} {% if c.seqincrement or c.seqcycle or c.seqincrement or c.seqstart or c.seqmin or c.seqmax or c.seqcache %} ( {% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/table_test_data.json b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/table_test_data.json index 65abbd928..f364d2f71 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/table_test_data.json +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/table_test_data.json @@ -2157,5 +2157,52 @@ }, "is_list": false } + ], + "table_create_geometry": [ + { + "name": "Create: Add Table with geometry columns", + "is_positive_test": true, + "inventory_data": {}, + "test_data": { + "description": "Create Table API Test", + "columns": [ + { + "name": "id", + "cltype": "bigint", + "attacl": [], + "is_primary_key": false, + "attnotnull": true, + "attlen": null, + "attprecision": null, + "attoptions": [], + "seclabels": [], + "colconstype": "n", + "attidentity": "a" + }, + { + "name": "geom", + "cltype": "geometry", + "attacl": [], + "is_primary_key": false, + "attnotnull": true, + "attlen": null, + "attprecision": null, + "attoptions": [], + "seclabels": [], + "colconstype": "n", + "attidentity": "a", + "geometry": "Point", + "srid": "2154" + } + ] + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + } ] } diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/test_geometry_table.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/test_geometry_table.py new file mode 100644 index 000000000..f6415147e --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/test_geometry_table.py @@ -0,0 +1,136 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2025, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import json +import uuid +from unittest.mock import patch + +from pgadmin.browser.server_groups.servers.databases.schemas.tests import \ + utils as schema_utils +from pgadmin.browser.server_groups.servers.databases.tests import utils as \ + database_utils +from pgadmin.utils import server_utils +from pgadmin.utils.route import BaseTestGenerator +from regression import parent_node_dict +from regression.python_test_utils import test_utils as utils +from . import utils as tables_utils + + +class TableGeometryTestCase(BaseTestGenerator): + """ This class will add new collation under schema node. """ + url = '/browser/table/obj/' + + # Generates scenarios + scenarios = utils.generate_scenarios("table_create_geometry", + tables_utils.test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + # Check server version + schema_info = parent_node_dict["schema"][-1] + self.server_id = schema_info["server_id"] + + if "server_min_version" in self.inventory_data: + server_con = server_utils.connect_server(self, self.server_id) + if not server_con["info"] == "Server connected.": + raise Exception("Could not connect to server to add " + "partitioned table.") + if server_con["data"]["version"] < \ + self.inventory_data["server_min_version"]: + self.skipTest(self.inventory_data["skip_msg"]) + + # Create db connection + self.db_name = parent_node_dict["database"][-1]["db_name"] + self.db_id = schema_info["db_id"] + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.server_id, self.db_id) + if not db_con['data']["connected"]: + raise Exception("Could not connect to database to add a table.") + + # Create schema + self.schema_id = schema_info["schema_id"] + self.schema_name = schema_info["schema_name"] + schema_response = schema_utils.verify_schemas(self.server, + self.db_name, + self.schema_name) + if not schema_response: + raise Exception("Could not find the schema to add a table.") + + tables_utils.create_postgis_extension(self) + + if not tables_utils.is_postgis_present(self): + self.skipTest(tables_utils.SKIP_MSG_EXTENSION) + + def runTest(self): + """ This function will add table under schema node. """ + if "table_name" in self.data: + self.table_name = self.data["table_name"] + else: + self.table_name = "test_table_add_%s" % (str(uuid.uuid4())[1:8]) + + db_user = self.server["username"] + + # Get the common data + self.data.update(tables_utils.get_table_common_data()) + if self.server_information and \ + 'server_version' in self.server_information and \ + self.server_information['server_version'] >= 120000: + self.data['spcname'] = None + self.data.update({ + "name": self.table_name, + "relowner": db_user, + "schema": self.schema_name, + "relacl": [{ + "grantee": db_user, + "grantor": db_user, + "privileges": [{ + "privilege_type": "a", + "privilege": True, + "with_grant": True + }, { + "privilege_type": "r", + "privilege": True, + "with_grant": False + }, { + "privilege_type": "w", + "privilege": True, + "with_grant": False + }] + }] + }) + + # Add table + if self.is_positive_test: + response = tables_utils.api_create(self) + + # Assert response + utils.assert_status_code(self, response) + + else: + if self.mocking_required: + with patch(self.mock_data["function_name"], + side_effect=eval(self.mock_data["return_value"])): + response = tables_utils.api_create(self) + else: + if self.table_name == "": + del self.data["name"] + response = tables_utils.api_create(self) + + # Assert response + utils.assert_status_code(self, response) + utils.assert_error_message(self, response) + + def tearDown(self): + # Drop the extension + tables_utils.drop_postgis_extension(self) + # Disconnect the database + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/utils.py index 9402f45b8..41cc9d3c5 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/utils.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/utils.py @@ -18,6 +18,7 @@ from regression.python_test_utils import test_utils as utils # Load test data from json file. CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +SKIP_MSG_EXTENSION = ("PostGIS extension is not installed in the database. ") with open(CURRENT_PATH + "/table_test_data.json") as data_file: test_cases = json.load(data_file) @@ -593,3 +594,73 @@ def get_table_id(server, db_name, table_name): except Exception: traceback.print_exc(file=sys.stderr) raise + + +def create_postgis_extension(self): + """ + This function will create postgis extension in the database. + :return: + """ + try: + connection = utils.get_db_connection(self.db_name, + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port'], + self.server['sslmode']) + pg_cursor = connection.cursor() + # Check if postgis extension is available to install + pg_cursor.execute('''SELECT COUNT(*) FROM pg_available_extensions WHERE + name ='postgis' ''') + res = pg_cursor.fetchone() + if res and len(res) > 0 and int(res[0]) == 1: + pg_cursor.execute('''CREATE EXTENSION IF NOT EXISTS postgis''') + connection.commit() + connection.close() + except Exception: + traceback.print_exc(file=sys.stderr) + + return False + + +def drop_postgis_extension(self): + """ + This function will drop postgis extension in the database. + :return: + """ + try: + connection = utils.get_db_connection(self.db_name, + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port'], + self.server['sslmode']) + pg_cursor = connection.cursor() + pg_cursor.execute('''DROP EXTENSION IF EXISTS postgis''') + connection.commit() + connection.close() + except Exception: + traceback.print_exc(file=sys.stderr) + + +def is_postgis_present(self): + """ + This function will check if postgis extension is present in the + database. + :return: + """ + try: + connection = utils.get_db_connection(self.db_name, + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port'], + self.server['sslmode']) + pg_cursor = connection.cursor() + res = utils.check_extension_exists(pg_cursor, 'postgis') + connection.close() + if res and len(res) > 0 and int(res[0]) == 1: + return True + except Exception: + traceback.print_exc(file=sys.stderr) + return False diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/utils.py index a34ad6194..485faf4f2 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/utils.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/utils.py @@ -18,6 +18,7 @@ from flask import render_template from pgadmin.browser.collection import CollectionNodeModule from pgadmin.utils.ajax import internal_server_error from pgadmin.utils.driver import get_driver +from pgadmin.utils import check_extension_exists from config import PG_DEFAULT_DRIVER from pgadmin.utils.constants import DATATYPE_TIME_WITH_TIMEZONE,\ DATATYPE_TIME_WITHOUT_TIMEZONE,\ @@ -192,6 +193,31 @@ class DataTypeReader: return True, res + def get_geometry_types(self, conn): + """ + Returns geometry types. + Args: + conn: Connection Object + """ + types = [] + status, res = check_extension_exists(conn, 'postgis') + + if not status: + return status, res + + if res: + sql = '''SELECT postgis_typmod_type(i) FROM + generate_series(4, 63) AS i;''' + status, rset = conn.execute_2darray(sql) + if not status: + return status, rset + for row in rset['rows']: + types.append({ + 'label': row['postgis_typmod_type'], + 'value': row['postgis_typmod_type'] + }) + return True, types + @staticmethod def get_length_precision(elemoid_or_name, typname=None): precision = False diff --git a/web/pgadmin/tools/erd/__init__.py b/web/pgadmin/tools/erd/__init__.py index d934158f5..39d1feeab 100644 --- a/web/pgadmin/tools/erd/__init__.py +++ b/web/pgadmin/tools/erd/__init__.py @@ -588,10 +588,16 @@ def prequisite(trans_id, sgid, sid, did): if not status: return internal_server_error(errormsg=schemas) + status, types = helper.get_geometry_types() + + if not status: + return internal_server_error(errormsg=types) + return make_json_response( data={ 'col_types': col_types, - 'schemas': schemas['rows'] + 'schemas': schemas['rows'], + 'geometry_types': types }, status=200 ) diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx index cb47c8bd4..54cece94e 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx +++ b/web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx @@ -399,7 +399,7 @@ export default class ERDTool extends React.Component { this.erdDialogs.showTableDialog({ title, attributes, isNew, tableNodes: this.diagram.getModel().getNodesDict(), colTypes: this.diagram.getCache('colTypes'), schemas: this.diagram.getCache('schemas'), - serverInfo, callback + geometryTypes: this.diagram.getCache('geometryTypes'),serverInfo, callback }); }; } else if(dialogName === 'onetomany_dialog' || dialogName === 'manytomany_dialog' || dialogName === 'onetoone_dialog') { @@ -875,6 +875,7 @@ export default class ERDTool extends React.Component { let data = response.data.data; this.diagram.setCache('colTypes', data['col_types']); this.diagram.setCache('schemas', data['schemas']); + this.diagram.setCache('geometryTypes', data['geometry_types']); return true; } catch (error) { this.handleAxiosCatch(error); diff --git a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/index.jsx b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/index.jsx index 5241cea75..96567213f 100644 --- a/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/index.jsx +++ b/web/pgadmin/tools/erd/static/js/erd_tool/dialogs/index.jsx @@ -35,7 +35,7 @@ export default class ERDDialogs { showTableDialog(params) { let schema = getTableDialogSchema( params.attributes, params.isNew, params.tableNodes, - params.colTypes, params.schemas); + params.colTypes, params.schemas, params.geometryTypes); this.modal.showModal(params.title, (closeModal)=>{ return (