Skip to content

Commit 2421814

Browse files
authored
Merge pull request #114 from Dan-Knott/dk/pg-exclude-extension-objects
postgresql: exclude extension-owned objects from introspection
2 parents f2d6a87 + 7c95395 commit 2421814

File tree

54 files changed

+322
-103
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+322
-103
lines changed

.github/workflows/test.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@ jobs:
2929
- postgres-drivername: postgresql+psycopg
3030
sqlalchemy-version: 1.4.0
3131

32-
env:
33-
PMR_POSTGRES_IMAGE: postgres:13
34-
3532
steps:
3633
- uses: actions/checkout@v3
3734

src/sqlalchemy_declarative_extensions/dialects/postgresql/query.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from sqlalchemy_declarative_extensions.dialects.postgresql.schema import (
2424
databases_query,
2525
default_acl_query,
26+
extensions_query,
2627
functions_query,
2728
object_acl_query,
2829
objects_query,
@@ -47,13 +48,33 @@
4748
from sqlalchemy_declarative_extensions.procedure import Procedure as BaseProcedure
4849
from sqlalchemy_declarative_extensions.sql import qualify_name
4950

51+
EXTENSION_SCHEMAS = {
52+
"postgis_topology": ["topology"],
53+
"postgis_tiger_geocoder": ["tiger", "tiger_data"],
54+
"pg_partman": ["partman"],
55+
"timescaledb": ["timescaledb_internal"],
56+
"pgmq": ["pgmq"],
57+
"pgrouting": ["pgrouting"],
58+
"orafce": ["orafce"],
59+
"pgagent": ["pgagent"],
60+
}
61+
EXTENSION_TRIGGERS = {
62+
"postgis_topology": {"layer_integrity_checks": "topology.layer"},
63+
}
64+
5065

5166
def get_schemas_postgresql(connection: Connection):
5267
from sqlalchemy_declarative_extensions.schema.base import Schema
5368

69+
extension_schemas = []
70+
for name, _ in connection.execute(extensions_query).fetchall():
71+
if name in EXTENSION_SCHEMAS:
72+
extension_schemas.extend(EXTENSION_SCHEMAS[name])
73+
5474
return {
5575
schema: Schema(schema)
5676
for schema, *_ in connection.execute(schemas_query).fetchall()
77+
if schema not in extension_schemas
5778
}
5879

5980

@@ -220,8 +241,18 @@ def get_functions_postgresql(connection: Connection) -> Sequence[BaseFunction]:
220241

221242
def get_triggers_postgresql(connection: Connection):
222243
triggers = []
244+
245+
extension_triggers = {}
246+
for name, _ in connection.execute(extensions_query).fetchall():
247+
if name in EXTENSION_TRIGGERS:
248+
extension_triggers.update(EXTENSION_TRIGGERS[name])
249+
223250
for t in connection.execute(triggers_query).fetchall():
224251
on = t.on_name if t.on_schema == "public" else f"{t.on_schema}.{t.on_name}"
252+
253+
if extension_triggers.get(t.name) == on:
254+
continue
255+
225256
execute = (
226257
t.execute_name
227258
if t.execute_schema == "public"

src/sqlalchemy_declarative_extensions/dialects/postgresql/schema.py

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
and_,
44
bindparam,
55
column,
6+
exists,
67
func,
78
literal,
89
table,
910
text,
1011
union,
1112
)
12-
from sqlalchemy.dialects.postgresql import ARRAY, CHAR
13+
from sqlalchemy.dialects.postgresql import ARRAY, CHAR, REGCLASS
1314

1415
from sqlalchemy_declarative_extensions.sqlalchemy import select
1516

@@ -39,6 +40,21 @@
3940
column("datname"),
4041
)
4142

43+
pg_depend = table(
44+
"pg_depend",
45+
column("objid"),
46+
column("classid"),
47+
column("deptype"),
48+
)
49+
50+
pg_extension = table(
51+
"pg_extension",
52+
column("oid"),
53+
column("extname"),
54+
column("extversion"),
55+
column("extconfig"),
56+
)
57+
4258
pg_namespace = table(
4359
"pg_namespace",
4460
column("oid"),
@@ -146,11 +162,24 @@ def _schema_not_pg(column=pg_namespace.c.nspname):
146162
)
147163

148164

165+
def _schema_not_from_extension(namespace_oid_column=pg_namespace.c.oid):
166+
return _not_from_extension(namespace_oid_column, "pg_namespace")
167+
168+
169+
def _not_from_extension(obj_id_col, class_name):
170+
return ~exists().where(pg_depend.c.objid == obj_id_col).where(
171+
pg_depend.c.classid == literal(class_name).cast(REGCLASS)
172+
).where(pg_depend.c.deptype == literal("e"))
173+
174+
149175
_schema_not_public = pg_namespace.c.nspname != "public"
150176
_table_not_pg = pg_class.c.relname.notlike("pg_%")
151177

152178
schemas_query = (
153-
select(pg_namespace.c.nspname).where(_schema_not_pg()).where(_schema_not_public)
179+
select(pg_namespace.c.nspname)
180+
.where(_schema_not_pg())
181+
.where(_schema_not_public)
182+
.where(_schema_not_from_extension())
154183
)
155184

156185

@@ -160,6 +189,10 @@ def _schema_not_pg(column=pg_namespace.c.nspname):
160189
.where(_schema_not_public)
161190
)
162191

192+
extensions_query = select(
193+
pg_extension.c.extname.label("name"), pg_extension.c.extversion.label("version")
194+
)
195+
163196
schema_exists_query = text(
164197
"SELECT schema_name FROM information_schema.schemata WHERE schema_name = :schema"
165198
)
@@ -238,26 +271,25 @@ def _schema_not_pg(column=pg_namespace.c.nspname):
238271
)
239272
.where(_table_not_pg)
240273
.where(_schema_not_pg())
274+
.where(_schema_not_from_extension())
241275
)
242276

243-
views_query = union(
277+
views_query = (
244278
select(
245-
pg_views.c.schemaname.label("schema"),
246-
pg_views.c.viewname.label("name"),
247-
pg_views.c.definition.label("definition"),
248-
literal(False).label("materialized"),
279+
pg_namespace.c.nspname.label("schema"),
280+
pg_class.c.relname.label("name"),
281+
func.pg_get_viewdef(pg_class.c.oid).label("definition"),
282+
(pg_class.c.relkind == literal("m")).label("materialized"),
249283
)
250-
.where(_schema_not_pg(pg_views.c.schemaname))
251-
.where(pg_views.c.viewname.notin_(["pg_stat_statements"])),
252-
select(
253-
pg_matviews.c.schemaname.label("schema"),
254-
pg_matviews.c.matviewname.label("name"),
255-
pg_matviews.c.definition.label("definition"),
256-
literal(True).label("materialized"),
257-
).where(_schema_not_pg(pg_matviews.c.schemaname)),
284+
.select_from(
285+
pg_class.join(pg_namespace, pg_class.c.relnamespace == pg_namespace.c.oid)
286+
)
287+
.where(pg_class.c.relkind.in_(["v", "m"]))
288+
.where(_schema_not_pg(pg_namespace.c.nspname))
289+
.where(_schema_not_from_extension())
290+
.where(_not_from_extension(pg_class.c.oid, "pg_class"))
258291
)
259292

260-
261293
views_subquery = views_query.cte()
262294
view_query = (
263295
select(views_subquery)
@@ -283,6 +315,8 @@ def _schema_not_pg(column=pg_namespace.c.nspname):
283315
)
284316
.where(pg_namespace.c.nspname.notin_(["pg_catalog", "information_schema"]))
285317
.where(pg_proc.c.prokind == "p")
318+
.where(_schema_not_from_extension())
319+
.where(_not_from_extension(pg_proc.c.oid, "pg_proc"))
286320
)
287321

288322
functions_query = (
@@ -302,6 +336,7 @@ def _schema_not_pg(column=pg_namespace.c.nspname):
302336
)
303337
.where(pg_namespace.c.nspname.notin_(["pg_catalog", "information_schema"]))
304338
.where(pg_proc.c.prokind != "p")
339+
.where(_not_from_extension(pg_proc.c.oid, "pg_proc"))
305340
)
306341

307342

@@ -335,4 +370,6 @@ def _schema_not_pg(column=pg_namespace.c.nspname):
335370
.join(proc_nsp, pg_proc.c.pronamespace == proc_nsp.c.oid)
336371
)
337372
.where(pg_trigger.c.tgisinternal.is_(False))
373+
.where(_schema_not_from_extension())
374+
.where(_not_from_extension(pg_trigger.c.oid, "pg_trigger"))
338375
)

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def pmr_postgres_container(pytestconfig, pmr_postgres_config: PostgresConfig):
3636

3737
@pytest.fixture
3838
def pmr_postgres_config():
39-
return PostgresConfig(port=None, ci_port=None)
39+
return PostgresConfig(image="postgres:13", port=None, ci_port=None)
4040

4141

4242
@pytest.fixture
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import pytest
2+
from pytest_mock_resources import PostgresConfig, create_postgres_fixture
3+
from sqlalchemy import func, select, text
4+
5+
from sqlalchemy_declarative_extensions import Functions, Schemas, Triggers, Views
6+
from sqlalchemy_declarative_extensions.function.compare import compare_functions
7+
from sqlalchemy_declarative_extensions.schema.compare import compare_schemas
8+
from sqlalchemy_declarative_extensions.trigger.compare import compare_triggers
9+
from sqlalchemy_declarative_extensions.view.compare import compare_views
10+
11+
pg = create_postgres_fixture(scope="function", engine_kwargs={"echo": True})
12+
13+
14+
@pytest.fixture
15+
def pmr_postgres_config():
16+
return PostgresConfig(image="postgis/postgis:13-3.5", port=None, ci_port=None)
17+
18+
19+
@pytest.fixture(autouse=True)
20+
def postgis_extension(pg):
21+
with pg.connect() as connection:
22+
connection.execute(text("CREATE EXTENSION postgis"))
23+
connection.execute(text("CREATE EXTENSION postgis_topology"))
24+
connection.commit()
25+
26+
27+
def test_functions(pg):
28+
with pg.connect() as connection:
29+
diff = compare_functions(connection, Functions())
30+
result = connection.execute(select(func.PostGis_Version())).scalar()
31+
assert result
32+
assert diff == []
33+
34+
35+
def test_schemas(pg):
36+
with pg.connect() as connection:
37+
diff = compare_schemas(connection, Schemas())
38+
assert diff == []
39+
40+
41+
def test_triggers(pg):
42+
with pg.connect() as connection:
43+
diff = compare_triggers(connection, Triggers())
44+
assert diff == []
45+
46+
47+
def test_views(pg):
48+
with pg.connect() as connection:
49+
diff = compare_views(connection, Views())
50+
result = connection.execute(
51+
text("SELECT * FROM geometry_columns")
52+
).one_or_none()
53+
assert not result
54+
assert diff == []

tests/examples/test_create_role/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66

77
@pytest.fixture(scope="session")
88
def pmr_postgres_config():
9-
return PostgresConfig(port=None, ci_port=None)
9+
return PostgresConfig(image="postgres:13", port=None, ci_port=None)

tests/examples/test_create_rows/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66

77
@pytest.fixture(scope="session")
88
def pmr_postgres_config():
9-
return PostgresConfig(port=None, ci_port=None)
9+
return PostgresConfig(image="postgres:13", port=None, ci_port=None)

tests/examples/test_delete_rows/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66

77
@pytest.fixture(scope="session")
88
def pmr_postgres_config():
9-
return PostgresConfig(port=None, ci_port=None)
9+
return PostgresConfig(image="postgres:13", port=None, ci_port=None)

tests/examples/test_delete_rows_ignore_unspecified/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66

77
@pytest.fixture(scope="session")
88
def pmr_postgres_config():
9-
return PostgresConfig(port=None, ci_port=None)
9+
return PostgresConfig(image="postgres:13", port=None, ci_port=None)

tests/examples/test_delete_unspecified_grants/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ def alembic_engine(_alembic_engine):
1212

1313
@pytest.fixture(scope="session")
1414
def pmr_postgres_config():
15-
return PostgresConfig(port=None, ci_port=None)
15+
return PostgresConfig(image="postgres:13", port=None, ci_port=None)

0 commit comments

Comments
 (0)