Add support for type constructors for PostGIS spatial types. #2256

pull/8686/head
Pravesh Sharma 2025-04-23 15:35:42 +05:30 committed by GitHub
parent 75dc42cf24
commit b08e90ea88
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 426 additions and 20 deletions

View File

@ -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' }}

View File

@ -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' }}

View File

@ -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):
"""

View File

@ -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,

View File

@ -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

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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": {}
}
}
]
}

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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
)

View File

@ -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);

View File

@ -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 (
<SchemaView

View File

@ -309,7 +309,11 @@ export class TableNodeWidget extends React.Component {
let cltype = col.cltype;
if(col.attlen) {
cltype += '('+ col.attlen + (col.attprecision ? ',' + col.attprecision : '') +')';
cltype += `(${col.attlen}${col.attprecision ? ','+col.attprecision :''})`;
}
if(col.geometry) {
cltype += `(${col.geometry}${col.srid ? ','+col.srid : ''})`;
}
return (

View File

@ -31,6 +31,10 @@ class ERDTableView(BaseTableView, DataTypeReader):
condition = self.get_types_condition_sql(False)
return DataTypeReader.get_types(self, self.conn, condition, True)
@BaseTableView.check_precondition
def get_geometry_types(self, conn_id=None, did=None, sid=None):
return DataTypeReader.get_geometry_types(self, self.conn)
@BaseTableView.check_precondition
def fetch_all_tables(self, did=None, sid=None, scid=None):
all_tables = []
@ -109,6 +113,11 @@ class ERDHelper:
return self.table_view.get_types(
conn_id=self.conn_id, did=self.did, sid=self.sid)
def get_geometry_types(self):
return self.table_view.get_geometry_types(
conn_id=self.conn_id, did=self.did, sid=self.sid
)
def get_table_sql(self, data, with_drop=False):
SQL, _ = self.table_view.sql(
conn_id=self.conn_id, did=self.did, sid=self.sid,

View File

@ -984,3 +984,16 @@ def get_safe_post_logout_redirect():
if url.startswith(item):
return url
return url_for('security.login')
def check_extension_exists(conn, extension_name):
sql = f"SELECT * FROM pg_extension WHERE extname = '{extension_name}'"
status, res = conn.execute_scalar(sql)
if status:
if res:
return status, True
else:
return status, False
else:
# If the query fails, we assume the extension does not exist
return status, res

View File

@ -1885,3 +1885,10 @@ def module_patch(*args):
# did not find a module, just return the default mock
return mock.patch(*args)
def check_extension_exists(cursor, extension_name):
cursor.execute(f"""SELECT COUNT(*) FROM pg_extension
WHERE extname='{extension_name}'""")
res = cursor.fetchone()
return res