pgdevkit 0.4.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/PKG-INFO +62 -23
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/README.md +61 -22
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/cli.py +68 -14
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/dialect.py +10 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/migrate.py +13 -53
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/parser.py +9 -2
- pgdevkit-0.5.0/pgdevkit/schemas.py +118 -0
- pgdevkit-0.5.0/pgdevkit/sql_text.py +56 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/testdb/api.py +49 -7
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/testdb/mssql/api.py +28 -4
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/testdb/schema.py +52 -17
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pyproject.toml +1 -1
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/test_migrate.py +2 -2
- pgdevkit-0.5.0/tests/test_migrate_schemas.py +52 -0
- pgdevkit-0.5.0/tests/test_parser_schemas.py +98 -0
- pgdevkit-0.5.0/tests/test_schemas.py +156 -0
- pgdevkit-0.5.0/tests/testdb/test_schema_filtering.py +115 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/uv.lock +1 -1
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/.github/workflows/auto-release.yml +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/.github/workflows/python-publish.yml +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/.github/workflows/python-test.yml +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/.gitignore +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/.python-version +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/docs/database-layout.md +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/__init__.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/areas.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/backends/__init__.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/backends/base.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/backends/mssql.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/backends/postgres.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/connection.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/db/__init__.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/db/complex_types.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/db/connection.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/db/crud.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/db/loader.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/db/model.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/db/mssql_crud.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/db/mssql_sql.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/diff.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/fetch_missing.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/introspect.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/lakebase.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/models.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/mssql_introspect.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/testdb/__init__.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/testdb/_docker.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/testdb/config.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/testdb/constants.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/testdb/container.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/testdb/mssql/__init__.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/testdb/mssql/constants.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/testdb/mssql/container.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/testdb/naming.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/pgdevkit/testdb/query.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/skills/pgdevkit/SKILL.md +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/skills/pgdevkit/references/dynamic-sql.md +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/skills/pgdevkit/references/temporal-tables.md +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/__init__.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/conftest.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/db/__init__.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/db/test_complex_types.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/db/test_connection.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/db/test_crud.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/db/test_loader.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/db/test_mssql_crud_live.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/db/test_mssql_crud_sql.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/fixtures/01_schema.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/fixtures/02_types.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/fixtures/03_tables.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/fixtures/04_views.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/fixtures/05_functions.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/fixtures/06_indexes.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/test_areas.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/test_cli_compare.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/test_compare.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/test_compare_mssql_live.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/test_connection.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/test_dialect.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/test_diff_mssql.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/test_fetch_missing.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/test_fetch_missing_cli.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/test_lakebase.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/test_migrate_areas.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/test_mssql_introspect.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/test_parser_areas.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/test_parser_mssql.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/__init__.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/conftest.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/fixtures/database/app/migrations/001_add_gadget_note.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/fixtures/database/app/tables/gadget.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/fixtures/database/app/tables/gadget.test_data.json +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/fixtures/database/app/tables/widget.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/fixtures/database/app/tables/widget.test_data.json +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/fixtures/database/app/tables/widget_part.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/fixtures/database/app/tables/widget_part_detail.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/fixtures/database/app/types/dimensions.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/fixtures/database/app/types/mood.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/fixtures/database/app/views/a_wrapper_view.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/fixtures/database/app/views/b_base_view.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/fixtures/database/schema/app.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/fixtures/database_mssql/app/tables/widget.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/fixtures/database_mssql/app/tables/widget.test_data.json +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/fixtures/database_mssql/app/views/a_wrapper_view.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/fixtures/database_mssql/app/views/b_base_view.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/fixtures/database_mssql/schema/app.sql +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/test_api.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/test_api_mssql.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/test_api_mssql_live.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/test_cli.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/test_config.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/test_config_mssql.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/test_constants.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/test_container.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/test_mssql_constants.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/test_naming.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/test_query.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/test_schema.py +0 -0
- {pgdevkit-0.4.0 → pgdevkit-0.5.0}/tests/testdb/test_schema_mssql.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pgdevkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A helper for developing with Postgres
|
|
5
5
|
Requires-Python: >=3.14
|
|
6
6
|
Requires-Dist: docker>=7.1.0
|
|
@@ -79,7 +79,13 @@ which parses/introspects/diffs like any other column type; see
|
|
|
79
79
|
handled on the CRUD side (write-side serialization only, no auto-parsing on
|
|
80
80
|
read — `mssql-python` doesn't distinguish `json` columns from `nvarchar`).
|
|
81
81
|
|
|
82
|
-
### Area
|
|
82
|
+
### Area and schema filtering
|
|
83
|
+
|
|
84
|
+
Two independent, composable ways to narrow which files a command touches:
|
|
85
|
+
**area** is an explicit opt-in tag; **schema** is derived automatically from
|
|
86
|
+
each file's own SQL.
|
|
87
|
+
|
|
88
|
+
#### Area tagging
|
|
83
89
|
|
|
84
90
|
Any migration file or `database/` code file can declare one or more areas by
|
|
85
91
|
starting with a `-- area:` comment:
|
|
@@ -97,43 +103,69 @@ first real statement); a `-- area:` comment later in the file doesn't count.
|
|
|
97
103
|
A file with no directive is untagged, and untagged files are treated as
|
|
98
104
|
shared/common.
|
|
99
105
|
|
|
100
|
-
|
|
106
|
+
#### Schema filtering
|
|
107
|
+
|
|
108
|
+
No tag needed — schema membership is parsed straight out of the SQL itself:
|
|
109
|
+
every schema-qualified (or default-schema, when unqualified) table/view/
|
|
110
|
+
function/index reference across every statement in the file, DDL or DML
|
|
111
|
+
alike, plus any `CREATE SCHEMA name`. A file whose schema(s) can't be
|
|
112
|
+
determined (unparseable content, or no table/schema reference in it at all)
|
|
113
|
+
is treated the same as an untagged file — always kept.
|
|
114
|
+
|
|
115
|
+
#### Options
|
|
116
|
+
|
|
117
|
+
`pgdb compare`, `pgdb migrate check`, `pgdb migrate apply`, `pgdb testdb up`,
|
|
118
|
+
and `pgdb testdb reset` all accept:
|
|
101
119
|
|
|
102
120
|
- `--area NAME` (repeatable) — restrict to files declaring one of the given
|
|
103
121
|
areas, **plus every untagged file** (untagged files always stay in scope).
|
|
104
122
|
- `--exclude-area NAME` (repeatable) — drop files declaring one of the given
|
|
105
123
|
areas; untagged files are never dropped by this.
|
|
124
|
+
- `--schema NAME` (repeatable) — restrict to files referencing one of the
|
|
125
|
+
given schemas, **plus every file with no detectable schema reference**.
|
|
126
|
+
- `--exclude-schema NAME` (repeatable) — drop files referencing one of the
|
|
127
|
+
given schemas; files with no detectable reference are never dropped.
|
|
106
128
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
129
|
+
All four can be combined — a file must pass every filter it's subject to (an
|
|
130
|
+
area match doesn't excuse a schema mismatch, and vice versa), and a file
|
|
131
|
+
matching both an included and an excluded value on the same axis is
|
|
132
|
+
excluded. Passing none of them applies no filtering (the default, unchanged
|
|
133
|
+
behavior).
|
|
110
134
|
|
|
111
135
|
```bash
|
|
112
136
|
pgdb migrate apply path/to/database/_migration_scripts --url ... --area billing
|
|
113
137
|
pgdb compare path/to/database/ --url ... --exclude-area reporting
|
|
138
|
+
pgdb migrate check path/to/database/_migration_scripts --url ... --schema billing --exclude-schema reporting
|
|
139
|
+
pgdb testdb up --schema billing
|
|
114
140
|
```
|
|
115
141
|
|
|
116
142
|
`compare`'s default report (no `--report-extra-db`) only checks that the
|
|
117
143
|
filtered scripts exist correctly in the DB, so it composes safely with area
|
|
118
|
-
filtering. Passing `--report-extra-db` together with
|
|
119
|
-
reports every DB object outside the filtered area(s)
|
|
120
|
-
scripts" — since the live database has no concept
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
144
|
+
and schema filtering. Passing `--report-extra-db` together with either kind
|
|
145
|
+
of filter also reports every DB object outside the filtered area(s)/
|
|
146
|
+
schema(s) as "missing in scripts" — since the live database has no concept
|
|
147
|
+
of areas, and isn't itself filtered by `--schema` either — only the scripts
|
|
148
|
+
side is filtered — so treat that combination's "missing in scripts" results
|
|
149
|
+
with that in mind (the CLI prints a warning when you combine them).
|
|
150
|
+
|
|
151
|
+
`pgdb fetch-missing` deliberately has **no** `--area`/`--exclude-area` (or
|
|
152
|
+
`--schema`/`--exclude-schema`): it diffs the full database against scripts
|
|
153
|
+
to find genuinely untracked objects, so narrowing the scripts side would
|
|
154
|
+
make every object tracked under a different area/schema look "missing" too
|
|
155
|
+
— and `--write` would then reconstruct a duplicate file for something that
|
|
156
|
+
already exists.
|
|
157
|
+
|
|
158
|
+
`pgdevkit.areas` exposes the tag-filtering logic for scripting:
|
|
131
159
|
`parse_areas`/`file_areas` read a file's declared areas, and
|
|
132
160
|
`area_allowed`/`filter_by_area` apply the `only`/`exclude` semantics above.
|
|
133
|
-
`pgdevkit.
|
|
134
|
-
`
|
|
135
|
-
|
|
136
|
-
|
|
161
|
+
`pgdevkit.schemas` exposes the equivalent for schema filtering:
|
|
162
|
+
`sql_schemas`/`file_schemas` detect a file's referenced schemas, and
|
|
163
|
+
`schema_allowed`/`filter_by_schema` apply the same `only`/`exclude`
|
|
164
|
+
semantics. `pgdevkit.migrate.list_migration_files`/`pending_migrations` and
|
|
165
|
+
`pgdevkit.parser.parse_directory` take both pairs of keyword arguments
|
|
166
|
+
(`areas`/`exclude_areas` and `schemas`/`exclude_schemas`);
|
|
167
|
+
`pgdevkit.fetch_missing.find_missing_objects` takes neither, for the reason
|
|
168
|
+
above.
|
|
137
169
|
|
|
138
170
|
## `pgdb testdb`
|
|
139
171
|
|
|
@@ -165,6 +197,13 @@ def ensure_test_postgres():
|
|
|
165
197
|
|
|
166
198
|
CLI: `pgdb testdb up|reset|run-sql|status|shell|clean`.
|
|
167
199
|
|
|
200
|
+
`up`/`reset` accept `--area`/`--exclude-area` and `--schema`/`--exclude-schema`
|
|
201
|
+
(see "Area and schema filtering" above) to scope which `database/` files get
|
|
202
|
+
applied — e.g. `pgdb testdb up --schema billing` for a test DB with only the
|
|
203
|
+
`billing` schema's tables/views/functions, without waiting on the rest of the
|
|
204
|
+
project's schema to apply. `ensure_testdb`/`reset_testdb` take the same
|
|
205
|
+
keyword arguments when called from Python (e.g. from a pytest fixture).
|
|
206
|
+
|
|
168
207
|
Container connection defaults (`localhost:54322`, `postgres`/`testpwd`) can
|
|
169
208
|
be overridden with `PGDEVKIT_TESTDB_HOST`, `PGDEVKIT_TESTDB_PORT`,
|
|
170
209
|
`PGDEVKIT_TESTDB_USER`, `PGDEVKIT_TESTDB_PASSWORD`. Before touching the
|
|
@@ -58,7 +58,13 @@ which parses/introspects/diffs like any other column type; see
|
|
|
58
58
|
handled on the CRUD side (write-side serialization only, no auto-parsing on
|
|
59
59
|
read — `mssql-python` doesn't distinguish `json` columns from `nvarchar`).
|
|
60
60
|
|
|
61
|
-
### Area
|
|
61
|
+
### Area and schema filtering
|
|
62
|
+
|
|
63
|
+
Two independent, composable ways to narrow which files a command touches:
|
|
64
|
+
**area** is an explicit opt-in tag; **schema** is derived automatically from
|
|
65
|
+
each file's own SQL.
|
|
66
|
+
|
|
67
|
+
#### Area tagging
|
|
62
68
|
|
|
63
69
|
Any migration file or `database/` code file can declare one or more areas by
|
|
64
70
|
starting with a `-- area:` comment:
|
|
@@ -76,43 +82,69 @@ first real statement); a `-- area:` comment later in the file doesn't count.
|
|
|
76
82
|
A file with no directive is untagged, and untagged files are treated as
|
|
77
83
|
shared/common.
|
|
78
84
|
|
|
79
|
-
|
|
85
|
+
#### Schema filtering
|
|
86
|
+
|
|
87
|
+
No tag needed — schema membership is parsed straight out of the SQL itself:
|
|
88
|
+
every schema-qualified (or default-schema, when unqualified) table/view/
|
|
89
|
+
function/index reference across every statement in the file, DDL or DML
|
|
90
|
+
alike, plus any `CREATE SCHEMA name`. A file whose schema(s) can't be
|
|
91
|
+
determined (unparseable content, or no table/schema reference in it at all)
|
|
92
|
+
is treated the same as an untagged file — always kept.
|
|
93
|
+
|
|
94
|
+
#### Options
|
|
95
|
+
|
|
96
|
+
`pgdb compare`, `pgdb migrate check`, `pgdb migrate apply`, `pgdb testdb up`,
|
|
97
|
+
and `pgdb testdb reset` all accept:
|
|
80
98
|
|
|
81
99
|
- `--area NAME` (repeatable) — restrict to files declaring one of the given
|
|
82
100
|
areas, **plus every untagged file** (untagged files always stay in scope).
|
|
83
101
|
- `--exclude-area NAME` (repeatable) — drop files declaring one of the given
|
|
84
102
|
areas; untagged files are never dropped by this.
|
|
103
|
+
- `--schema NAME` (repeatable) — restrict to files referencing one of the
|
|
104
|
+
given schemas, **plus every file with no detectable schema reference**.
|
|
105
|
+
- `--exclude-schema NAME` (repeatable) — drop files referencing one of the
|
|
106
|
+
given schemas; files with no detectable reference are never dropped.
|
|
85
107
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
108
|
+
All four can be combined — a file must pass every filter it's subject to (an
|
|
109
|
+
area match doesn't excuse a schema mismatch, and vice versa), and a file
|
|
110
|
+
matching both an included and an excluded value on the same axis is
|
|
111
|
+
excluded. Passing none of them applies no filtering (the default, unchanged
|
|
112
|
+
behavior).
|
|
89
113
|
|
|
90
114
|
```bash
|
|
91
115
|
pgdb migrate apply path/to/database/_migration_scripts --url ... --area billing
|
|
92
116
|
pgdb compare path/to/database/ --url ... --exclude-area reporting
|
|
117
|
+
pgdb migrate check path/to/database/_migration_scripts --url ... --schema billing --exclude-schema reporting
|
|
118
|
+
pgdb testdb up --schema billing
|
|
93
119
|
```
|
|
94
120
|
|
|
95
121
|
`compare`'s default report (no `--report-extra-db`) only checks that the
|
|
96
122
|
filtered scripts exist correctly in the DB, so it composes safely with area
|
|
97
|
-
filtering. Passing `--report-extra-db` together with
|
|
98
|
-
reports every DB object outside the filtered area(s)
|
|
99
|
-
scripts" — since the live database has no concept
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
and schema filtering. Passing `--report-extra-db` together with either kind
|
|
124
|
+
of filter also reports every DB object outside the filtered area(s)/
|
|
125
|
+
schema(s) as "missing in scripts" — since the live database has no concept
|
|
126
|
+
of areas, and isn't itself filtered by `--schema` either — only the scripts
|
|
127
|
+
side is filtered — so treat that combination's "missing in scripts" results
|
|
128
|
+
with that in mind (the CLI prints a warning when you combine them).
|
|
129
|
+
|
|
130
|
+
`pgdb fetch-missing` deliberately has **no** `--area`/`--exclude-area` (or
|
|
131
|
+
`--schema`/`--exclude-schema`): it diffs the full database against scripts
|
|
132
|
+
to find genuinely untracked objects, so narrowing the scripts side would
|
|
133
|
+
make every object tracked under a different area/schema look "missing" too
|
|
134
|
+
— and `--write` would then reconstruct a duplicate file for something that
|
|
135
|
+
already exists.
|
|
136
|
+
|
|
137
|
+
`pgdevkit.areas` exposes the tag-filtering logic for scripting:
|
|
110
138
|
`parse_areas`/`file_areas` read a file's declared areas, and
|
|
111
139
|
`area_allowed`/`filter_by_area` apply the `only`/`exclude` semantics above.
|
|
112
|
-
`pgdevkit.
|
|
113
|
-
`
|
|
114
|
-
|
|
115
|
-
|
|
140
|
+
`pgdevkit.schemas` exposes the equivalent for schema filtering:
|
|
141
|
+
`sql_schemas`/`file_schemas` detect a file's referenced schemas, and
|
|
142
|
+
`schema_allowed`/`filter_by_schema` apply the same `only`/`exclude`
|
|
143
|
+
semantics. `pgdevkit.migrate.list_migration_files`/`pending_migrations` and
|
|
144
|
+
`pgdevkit.parser.parse_directory` take both pairs of keyword arguments
|
|
145
|
+
(`areas`/`exclude_areas` and `schemas`/`exclude_schemas`);
|
|
146
|
+
`pgdevkit.fetch_missing.find_missing_objects` takes neither, for the reason
|
|
147
|
+
above.
|
|
116
148
|
|
|
117
149
|
## `pgdb testdb`
|
|
118
150
|
|
|
@@ -144,6 +176,13 @@ def ensure_test_postgres():
|
|
|
144
176
|
|
|
145
177
|
CLI: `pgdb testdb up|reset|run-sql|status|shell|clean`.
|
|
146
178
|
|
|
179
|
+
`up`/`reset` accept `--area`/`--exclude-area` and `--schema`/`--exclude-schema`
|
|
180
|
+
(see "Area and schema filtering" above) to scope which `database/` files get
|
|
181
|
+
applied — e.g. `pgdb testdb up --schema billing` for a test DB with only the
|
|
182
|
+
`billing` schema's tables/views/functions, without waiting on the rest of the
|
|
183
|
+
project's schema to apply. `ensure_testdb`/`reset_testdb` take the same
|
|
184
|
+
keyword arguments when called from Python (e.g. from a pytest fixture).
|
|
185
|
+
|
|
147
186
|
Container connection defaults (`localhost:54322`, `postgres`/`testpwd`) can
|
|
148
187
|
be overridden with `PGDEVKIT_TESTDB_HOST`, `PGDEVKIT_TESTDB_PORT`,
|
|
149
188
|
`PGDEVKIT_TESTDB_USER`, `PGDEVKIT_TESTDB_PASSWORD`. Before touching the
|
|
@@ -29,9 +29,21 @@ _AREA_OPTION = typer.Option(
|
|
|
29
29
|
_EXCLUDE_AREA_OPTION = typer.Option(
|
|
30
30
|
[], "--exclude-area", help="Skip files declaring this area (repeatable); untagged files are never excluded"
|
|
31
31
|
)
|
|
32
|
+
_SCHEMA_OPTION = typer.Option(
|
|
33
|
+
[],
|
|
34
|
+
"--schema",
|
|
35
|
+
help="Restrict to files referencing this DB schema (repeatable); "
|
|
36
|
+
"files with no detectable schema reference always stay in scope",
|
|
37
|
+
)
|
|
38
|
+
_EXCLUDE_SCHEMA_OPTION = typer.Option(
|
|
39
|
+
[],
|
|
40
|
+
"--exclude-schema",
|
|
41
|
+
help="Skip files referencing this DB schema (repeatable); "
|
|
42
|
+
"files with no detectable schema reference are never excluded",
|
|
43
|
+
)
|
|
32
44
|
|
|
33
45
|
|
|
34
|
-
def
|
|
46
|
+
def _as_set(values: list[str]) -> frozenset[str] | None:
|
|
35
47
|
return frozenset(values) if values else None
|
|
36
48
|
|
|
37
49
|
testdb_app = typer.Typer(name="testdb", help="Manage the shared local Postgres test container")
|
|
@@ -59,17 +71,20 @@ def compare(
|
|
|
59
71
|
dialect: str = typer.Option("postgres", "--dialect", help="postgres (default) or mssql"),
|
|
60
72
|
area: list[str] = _AREA_OPTION,
|
|
61
73
|
exclude_area: list[str] = _EXCLUDE_AREA_OPTION,
|
|
74
|
+
schema: list[str] = _SCHEMA_OPTION,
|
|
75
|
+
exclude_schema: list[str] = _EXCLUDE_SCHEMA_OPTION,
|
|
62
76
|
scripts_dir: Path = typer.Argument(..., help="Directory containing SQL scripts"),
|
|
63
77
|
) -> None:
|
|
64
78
|
"""Compare SQL scripts to a live database and report differences."""
|
|
65
79
|
if not scripts_dir.is_dir():
|
|
66
80
|
err_console.print(f"[red]Error:[/red] {scripts_dir} is not a directory")
|
|
67
81
|
raise typer.Exit(2)
|
|
68
|
-
if report_extra_db and (area or exclude_area):
|
|
82
|
+
if report_extra_db and (area or exclude_area or schema or exclude_schema):
|
|
69
83
|
console.print(
|
|
70
|
-
"[yellow]⚠[/yellow] --report-extra-db with --area/--exclude-area will
|
|
71
|
-
"outside the filtered area(s) as \"missing in scripts\", since the
|
|
72
|
-
"of areas —
|
|
84
|
+
"[yellow]⚠[/yellow] --report-extra-db with --area/--exclude-area/--schema/--exclude-schema will "
|
|
85
|
+
"report every DB object outside the filtered area(s)/schema(s) as \"missing in scripts\", since the "
|
|
86
|
+
"live database has no concept of areas — and isn't itself filtered by --schema either — only the "
|
|
87
|
+
"scripts side is filtered."
|
|
73
88
|
)
|
|
74
89
|
|
|
75
90
|
try:
|
|
@@ -91,7 +106,12 @@ def compare(
|
|
|
91
106
|
|
|
92
107
|
with console.status("Parsing SQL scripts..."):
|
|
93
108
|
scripts_schema = parse_directory(
|
|
94
|
-
scripts_dir,
|
|
109
|
+
scripts_dir,
|
|
110
|
+
dialect=backend.dialect,
|
|
111
|
+
areas=_as_set(area),
|
|
112
|
+
exclude_areas=_as_set(exclude_area),
|
|
113
|
+
schemas=_as_set(schema),
|
|
114
|
+
exclude_schemas=_as_set(exclude_schema),
|
|
95
115
|
)
|
|
96
116
|
|
|
97
117
|
with console.status("Introspecting database..."):
|
|
@@ -188,17 +208,33 @@ def fetch_missing(
|
|
|
188
208
|
|
|
189
209
|
|
|
190
210
|
@testdb_app.command("up")
|
|
191
|
-
def testdb_up(
|
|
211
|
+
def testdb_up(
|
|
212
|
+
area: list[str] = _AREA_OPTION,
|
|
213
|
+
exclude_area: list[str] = _EXCLUDE_AREA_OPTION,
|
|
214
|
+
schema: list[str] = _SCHEMA_OPTION,
|
|
215
|
+
exclude_schema: list[str] = _EXCLUDE_SCHEMA_OPTION,
|
|
216
|
+
) -> None:
|
|
192
217
|
"""Ensure the container is running, the workspace DB exists, and schema is applied."""
|
|
193
|
-
testdb.ensure_testdb(
|
|
218
|
+
testdb.ensure_testdb(
|
|
219
|
+
areas=_as_set(area), exclude_areas=_as_set(exclude_area), schemas=_as_set(schema),
|
|
220
|
+
exclude_schemas=_as_set(exclude_schema),
|
|
221
|
+
)
|
|
194
222
|
info = testdb.status()
|
|
195
223
|
console.print(f"[green]Test DB ready:[/green] {info['database']} ({info['dsn']})")
|
|
196
224
|
|
|
197
225
|
|
|
198
226
|
@testdb_app.command("reset")
|
|
199
|
-
def testdb_reset(
|
|
227
|
+
def testdb_reset(
|
|
228
|
+
area: list[str] = _AREA_OPTION,
|
|
229
|
+
exclude_area: list[str] = _EXCLUDE_AREA_OPTION,
|
|
230
|
+
schema: list[str] = _SCHEMA_OPTION,
|
|
231
|
+
exclude_schema: list[str] = _EXCLUDE_SCHEMA_OPTION,
|
|
232
|
+
) -> None:
|
|
200
233
|
"""Drop and recreate only this workspace's database, then reapply schema + seed data."""
|
|
201
|
-
testdb.reset_testdb(
|
|
234
|
+
testdb.reset_testdb(
|
|
235
|
+
areas=_as_set(area), exclude_areas=_as_set(exclude_area), schemas=_as_set(schema),
|
|
236
|
+
exclude_schemas=_as_set(exclude_schema),
|
|
237
|
+
)
|
|
202
238
|
info = testdb.status()
|
|
203
239
|
console.print(f"[green]Test DB reset:[/green] {info['database']}")
|
|
204
240
|
|
|
@@ -272,6 +308,8 @@ def migrate_check(
|
|
|
272
308
|
),
|
|
273
309
|
area: list[str] = _AREA_OPTION,
|
|
274
310
|
exclude_area: list[str] = _EXCLUDE_AREA_OPTION,
|
|
311
|
+
schema: list[str] = _SCHEMA_OPTION,
|
|
312
|
+
exclude_schema: list[str] = _EXCLUDE_SCHEMA_OPTION,
|
|
275
313
|
) -> None:
|
|
276
314
|
"""List which migration files under migrations_dir are applied vs. pending."""
|
|
277
315
|
if not migrations_dir.is_dir():
|
|
@@ -281,7 +319,11 @@ def migrate_check(
|
|
|
281
319
|
conninfo = build_conninfo(url, entra_user)
|
|
282
320
|
tracking_table = tracking_table or migrate.default_tracking_table(migrations_dir)
|
|
283
321
|
local_files = migrate.list_migration_files(
|
|
284
|
-
migrations_dir,
|
|
322
|
+
migrations_dir,
|
|
323
|
+
areas=_as_set(area),
|
|
324
|
+
exclude_areas=_as_set(exclude_area),
|
|
325
|
+
schemas=_as_set(schema),
|
|
326
|
+
exclude_schemas=_as_set(exclude_schema),
|
|
285
327
|
)
|
|
286
328
|
try:
|
|
287
329
|
applied = migrate.applied_migrations(conninfo, tracking_table)
|
|
@@ -323,6 +365,8 @@ def migrate_apply(
|
|
|
323
365
|
yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirm-target prompt"),
|
|
324
366
|
area: list[str] = _AREA_OPTION,
|
|
325
367
|
exclude_area: list[str] = _EXCLUDE_AREA_OPTION,
|
|
368
|
+
schema: list[str] = _SCHEMA_OPTION,
|
|
369
|
+
exclude_schema: list[str] = _EXCLUDE_SCHEMA_OPTION,
|
|
326
370
|
) -> None:
|
|
327
371
|
"""Apply pending migration files, in filename order, tracking each in tracking_table."""
|
|
328
372
|
if not migrations_dir.is_dir():
|
|
@@ -331,7 +375,8 @@ def migrate_apply(
|
|
|
331
375
|
|
|
332
376
|
conninfo = build_conninfo(url, entra_user)
|
|
333
377
|
tracking_table = tracking_table or migrate.default_tracking_table(migrations_dir)
|
|
334
|
-
areas, exclude_areas =
|
|
378
|
+
areas, exclude_areas = _as_set(area), _as_set(exclude_area)
|
|
379
|
+
schemas, exclude_schemas = _as_set(schema), _as_set(exclude_schema)
|
|
335
380
|
target_desc = url.rsplit("@", 1)[-1] if "@" in url else url
|
|
336
381
|
if not yes:
|
|
337
382
|
typer.confirm(f"About to run migrations against {target_desc}. Continue?", abort=True)
|
|
@@ -341,13 +386,22 @@ def migrate_apply(
|
|
|
341
386
|
else:
|
|
342
387
|
try:
|
|
343
388
|
targets = migrate.pending_migrations(
|
|
344
|
-
migrations_dir,
|
|
389
|
+
migrations_dir,
|
|
390
|
+
conninfo,
|
|
391
|
+
tracking_table,
|
|
392
|
+
areas=areas,
|
|
393
|
+
exclude_areas=exclude_areas,
|
|
394
|
+
schemas=schemas,
|
|
395
|
+
exclude_schemas=exclude_schemas,
|
|
345
396
|
)
|
|
346
397
|
except migrate.TrackingTableMissing:
|
|
347
398
|
err_console.print(
|
|
348
399
|
f"[yellow]⚠[/yellow] {tracking_table} not found — treating every migration as pending"
|
|
349
400
|
)
|
|
350
|
-
targets = migrate.list_migration_files(
|
|
401
|
+
targets = migrate.list_migration_files(
|
|
402
|
+
migrations_dir, areas=areas, exclude_areas=exclude_areas, schemas=schemas,
|
|
403
|
+
exclude_schemas=exclude_schemas,
|
|
404
|
+
)
|
|
351
405
|
|
|
352
406
|
if not targets:
|
|
353
407
|
console.print("No pending migrations.")
|
|
@@ -43,6 +43,16 @@ _MSSQL_TYPE_SYNONYMS = {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
# Schemas that hold system catalog views/tables, never a file any project
|
|
47
|
+
# using pgdevkit manages -- a reference to one (e.g. an idempotency guard
|
|
48
|
+
# querying it, or a `SELECT ... FROM information_schema/pg_catalog/sys ...`)
|
|
49
|
+
# is never a real schema-membership or cross-file-dependency signal. Shared
|
|
50
|
+
# by `schemas.py` (schema-reference filtering) and `testdb/schema.py`
|
|
51
|
+
# (dependency-safe apply ordering), which both walk the same sqlglot Table
|
|
52
|
+
# nodes for a related-but-different purpose.
|
|
53
|
+
SYSTEM_SCHEMAS = {"pg_catalog", "information_schema", "sys"}
|
|
54
|
+
|
|
55
|
+
|
|
46
56
|
@dataclass(frozen=True)
|
|
47
57
|
class Dialect:
|
|
48
58
|
"""A thin wrapper around a sqlglot dialect name plus the handful of
|
|
@@ -19,6 +19,8 @@ from psycopg import errors as pg_errors
|
|
|
19
19
|
from psycopg import sql as pg_sql
|
|
20
20
|
|
|
21
21
|
from .areas import filter_by_area
|
|
22
|
+
from .schemas import filter_by_schema
|
|
23
|
+
from .sql_text import strip_line_comments
|
|
22
24
|
|
|
23
25
|
_IDENTIFIER = r"[A-Za-z_][A-Za-z0-9_]*"
|
|
24
26
|
_DEFAULT_TRACKING_TABLE = "public.schema_migrations"
|
|
@@ -124,61 +126,12 @@ def _split_sql(sql: str) -> list[str]:
|
|
|
124
126
|
return stmts
|
|
125
127
|
|
|
126
128
|
|
|
127
|
-
def _strip_line_comments(sql: str) -> str:
|
|
128
|
-
"""Drop '--' line comments, respecting string literals and $$...$$ blocks."""
|
|
129
|
-
buf: list[str] = []
|
|
130
|
-
i = 0
|
|
131
|
-
in_string = False
|
|
132
|
-
in_line_comment = False
|
|
133
|
-
dollar_tag: str | None = None
|
|
134
|
-
|
|
135
|
-
while i < len(sql):
|
|
136
|
-
c = sql[i]
|
|
137
|
-
if in_line_comment:
|
|
138
|
-
if c == "\n":
|
|
139
|
-
in_line_comment = False
|
|
140
|
-
buf.append(c)
|
|
141
|
-
elif dollar_tag is not None:
|
|
142
|
-
buf.append(c)
|
|
143
|
-
if c == "$" and sql[i:i + len(dollar_tag)] == dollar_tag:
|
|
144
|
-
buf.extend(list(dollar_tag[1:]))
|
|
145
|
-
i += len(dollar_tag)
|
|
146
|
-
dollar_tag = None
|
|
147
|
-
continue
|
|
148
|
-
elif in_string:
|
|
149
|
-
if c == "'" and i + 1 < len(sql) and sql[i + 1] == "'":
|
|
150
|
-
buf.append(c)
|
|
151
|
-
buf.append(sql[i + 1])
|
|
152
|
-
i += 2
|
|
153
|
-
continue
|
|
154
|
-
elif c == "'":
|
|
155
|
-
in_string = False
|
|
156
|
-
buf.append(c)
|
|
157
|
-
elif c == "-" and i + 1 < len(sql) and sql[i + 1] == "-":
|
|
158
|
-
in_line_comment = True
|
|
159
|
-
elif c == "$":
|
|
160
|
-
m = re.match(r"\$([A-Za-z0-9_]*)\$", sql[i:])
|
|
161
|
-
if m:
|
|
162
|
-
dollar_tag = m.group(0)
|
|
163
|
-
buf.extend(list(dollar_tag))
|
|
164
|
-
i += len(dollar_tag)
|
|
165
|
-
continue
|
|
166
|
-
buf.append(c)
|
|
167
|
-
elif c == "'":
|
|
168
|
-
in_string = True
|
|
169
|
-
buf.append(c)
|
|
170
|
-
else:
|
|
171
|
-
buf.append(c)
|
|
172
|
-
i += 1
|
|
173
|
-
return "".join(buf)
|
|
174
|
-
|
|
175
|
-
|
|
176
129
|
def _created_table_names(stmts: list[str]) -> list[str]:
|
|
177
130
|
"""Names of tables any CREATE TABLE statement targets, parsed via sqlglot (falls back to
|
|
178
131
|
regex on comment-stripped text for statements sqlglot's postgres dialect can't parse)."""
|
|
179
132
|
names: list[str] = []
|
|
180
133
|
for stmt in stmts:
|
|
181
|
-
stripped =
|
|
134
|
+
stripped = strip_line_comments(stmt)
|
|
182
135
|
if not re.search(r"CREATE\s+TABLE", stripped, re.IGNORECASE):
|
|
183
136
|
continue # skip sqlglot entirely for statements that can't be a CREATE TABLE
|
|
184
137
|
try:
|
|
@@ -220,7 +173,7 @@ def _idempotent_target(stmt: str) -> tuple[str, ...] | None:
|
|
|
220
173
|
COLUMN. None if the statement isn't one of these shapes — including any CREATE OR
|
|
221
174
|
REPLACE, which is never safe to treat as a no-op just because the object exists, since
|
|
222
175
|
the migration could be replacing it with different content."""
|
|
223
|
-
stripped =
|
|
176
|
+
stripped = strip_line_comments(stmt).strip()
|
|
224
177
|
if re.search(r"\bOR\s+REPLACE\b", stripped, re.IGNORECASE):
|
|
225
178
|
return None
|
|
226
179
|
|
|
@@ -281,9 +234,12 @@ def list_migration_files(
|
|
|
281
234
|
*,
|
|
282
235
|
areas: frozenset[str] | None = None,
|
|
283
236
|
exclude_areas: frozenset[str] | None = None,
|
|
237
|
+
schemas: frozenset[str] | None = None,
|
|
238
|
+
exclude_schemas: frozenset[str] | None = None,
|
|
284
239
|
) -> list[Path]:
|
|
285
240
|
files = sorted(migrations_dir.glob("*.sql"))
|
|
286
|
-
|
|
241
|
+
files = filter_by_area(files, only=areas, exclude=exclude_areas)
|
|
242
|
+
return filter_by_schema(files, only=schemas, exclude=exclude_schemas)
|
|
287
243
|
|
|
288
244
|
|
|
289
245
|
def applied_migrations(conninfo: str, tracking_table: str) -> dict[str, tuple[datetime, str]]:
|
|
@@ -306,9 +262,13 @@ def pending_migrations(
|
|
|
306
262
|
*,
|
|
307
263
|
areas: frozenset[str] | None = None,
|
|
308
264
|
exclude_areas: frozenset[str] | None = None,
|
|
265
|
+
schemas: frozenset[str] | None = None,
|
|
266
|
+
exclude_schemas: frozenset[str] | None = None,
|
|
309
267
|
) -> list[Path]:
|
|
310
268
|
applied = applied_migrations(conninfo, tracking_table)
|
|
311
|
-
files = list_migration_files(
|
|
269
|
+
files = list_migration_files(
|
|
270
|
+
migrations_dir, areas=areas, exclude_areas=exclude_areas, schemas=schemas, exclude_schemas=exclude_schemas
|
|
271
|
+
)
|
|
312
272
|
return [p for p in files if p.name not in applied]
|
|
313
273
|
|
|
314
274
|
|
|
@@ -10,6 +10,7 @@ import sqlglot.expressions as exp
|
|
|
10
10
|
|
|
11
11
|
from .areas import area_allowed, parse_areas
|
|
12
12
|
from .dialect import Dialect, POSTGRES, resolve_dialect
|
|
13
|
+
from .schemas import schema_allowed, sql_schemas
|
|
13
14
|
from .models import (
|
|
14
15
|
ColumnDef, ConstraintDef, CompositeTypeDef, DatabaseSchema,
|
|
15
16
|
EnumDef, FunctionDef, IndexDef, TableDef, ViewDef,
|
|
@@ -55,15 +56,21 @@ def parse_directory(
|
|
|
55
56
|
dialect: str | Dialect = "postgres",
|
|
56
57
|
areas: frozenset[str] | None = None,
|
|
57
58
|
exclude_areas: frozenset[str] | None = None,
|
|
59
|
+
schemas: frozenset[str] | None = None,
|
|
60
|
+
exclude_schemas: frozenset[str] | None = None,
|
|
58
61
|
) -> DatabaseSchema:
|
|
59
62
|
resolved = resolve_dialect(dialect)
|
|
60
63
|
db_schema = DatabaseSchema()
|
|
61
64
|
for sql_file in sorted(_iter_sql_files(scripts_dir)):
|
|
62
|
-
# Read once and reuse for
|
|
63
|
-
# filtering the file list up front (which would need its own read).
|
|
65
|
+
# Read once and reuse for the area/schema checks and parsing, rather
|
|
66
|
+
# than filtering the file list up front (which would need its own read).
|
|
64
67
|
content = sql_file.read_text(encoding="utf-8")
|
|
65
68
|
if (areas or exclude_areas) and not area_allowed(parse_areas(content), only=areas, exclude=exclude_areas):
|
|
66
69
|
continue
|
|
70
|
+
if (schemas or exclude_schemas) and not schema_allowed(
|
|
71
|
+
sql_schemas(content, resolved), only=schemas, exclude=exclude_schemas
|
|
72
|
+
):
|
|
73
|
+
continue
|
|
67
74
|
_parse_file(sql_file, db_schema, resolved, content=content)
|
|
68
75
|
return db_schema
|
|
69
76
|
|