pgdevkit 0.6.0__tar.gz → 0.6.2__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.6.0 → pgdevkit-0.6.2}/PKG-INFO +1 -1
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/docs/database-layout.md +11 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/testdb/constants.py +7 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/testdb/container.py +6 -1
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/testdb/schema.py +23 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pyproject.toml +1 -1
- pgdevkit-0.6.2/tests/testdb/fixtures/database/app/tables/event.sql +10 -0
- pgdevkit-0.6.2/tests/testdb/fixtures/database/app/tables/event_kind.sql +4 -0
- pgdevkit-0.6.2/tests/testdb/fixtures/database/permissions/grants.sql +18 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/test_container.py +40 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/test_schema.py +28 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/uv.lock +1 -1
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/.github/workflows/auto-release.yml +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/.github/workflows/python-publish.yml +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/.github/workflows/python-test.yml +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/.gitignore +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/.python-version +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/README.md +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/__init__.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/areas.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/backends/__init__.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/backends/base.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/backends/mssql.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/backends/postgres.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/cli.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/connection.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/db/__init__.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/db/complex_types.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/db/connection.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/db/crud.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/db/loader.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/db/model.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/db/mssql_crud.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/db/mssql_sql.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/dialect.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/diff.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/envtag.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/fetch_missing.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/introspect.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/lakebase.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/migrate.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/models.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/mssql_introspect.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/parser.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/schemas.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/sql_text.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/testdb/__init__.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/testdb/_docker.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/testdb/api.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/testdb/config.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/testdb/mssql/__init__.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/testdb/mssql/api.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/testdb/mssql/constants.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/testdb/mssql/container.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/testdb/naming.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/pgdevkit/testdb/query.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/skills/pgdevkit/SKILL.md +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/skills/pgdevkit/references/dynamic-sql.md +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/skills/pgdevkit/references/temporal-tables.md +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/__init__.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/conftest.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/db/__init__.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/db/test_complex_types.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/db/test_connection.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/db/test_crud.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/db/test_loader.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/db/test_mssql_crud_live.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/db/test_mssql_crud_sql.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/fixtures/01_schema.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/fixtures/02_types.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/fixtures/03_tables.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/fixtures/04_views.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/fixtures/05_functions.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/fixtures/06_indexes.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_areas.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_cli_compare.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_compare.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_compare_mssql_live.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_connection.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_dialect.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_diff_mssql.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_envtag.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_fetch_missing.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_fetch_missing_cli.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_lakebase.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_migrate.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_migrate_areas.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_migrate_env.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_migrate_schemas.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_mssql_introspect.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_parser_areas.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_parser_mssql.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_parser_schemas.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/test_schemas.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/__init__.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/conftest.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database/app/migrations/001_add_gadget_note.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database/app/tables/gadget.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database/app/tables/gadget.test_data.json +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database/app/tables/prod_only.prod.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database/app/tables/widget.init.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database/app/tables/widget.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database/app/tables/widget.test_data.json +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database/app/tables/widget_part.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database/app/tables/widget_part_detail.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database/app/types/dimensions.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database/app/types/mood.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database/app/views/a_wrapper_view.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database/app/views/b_base_view.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database/schema/app.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database_mssql/app/tables/widget.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database_mssql/app/tables/widget.test_data.json +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database_mssql/app/views/a_wrapper_view.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database_mssql/app/views/b_base_view.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database_mssql/schema/app.sql +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/test_api.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/test_api_mssql.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/test_api_mssql_live.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/test_cli.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/test_config.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/test_config_mssql.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/test_constants.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/test_mssql_constants.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/test_naming.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/test_query.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/test_schema_filtering.py +0 -0
- {pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/test_schema_mssql.py +0 -0
|
@@ -63,6 +63,17 @@ both together.
|
|
|
63
63
|
One object per file: `tables/user.sql`, `views/all_edits.sql`,
|
|
64
64
|
`types/measurement_unit.sql`.
|
|
65
65
|
|
|
66
|
+
**`permissions` files always apply last, genuinely.** A blanket `GRANT ...
|
|
67
|
+
ON ALL TABLES IN SCHEMA x TO role;` only covers what exists at the moment it
|
|
68
|
+
runs — so if a table with a cross-file dependency that isn't resolved on the
|
|
69
|
+
first pass (e.g. an FK to a table in another layer directory whose filename
|
|
70
|
+
happens to sort later) got created *after* a `permissions` file, that grant
|
|
71
|
+
would silently never cover it. `pgdb testdb` holds every `permissions` file
|
|
72
|
+
back unconditionally and applies it only once the whole rest of the tree
|
|
73
|
+
(delayed-retry resolution included) has actually finished — so write
|
|
74
|
+
`permissions` files as if every table/view they reference is guaranteed to
|
|
75
|
+
already exist, because it is.
|
|
76
|
+
|
|
66
77
|
---
|
|
67
78
|
|
|
68
79
|
## File-naming conventions
|
|
@@ -11,6 +11,13 @@ PORT = int(os.environ.get("PGDEVKIT_TESTDB_PORT", "54322"))
|
|
|
11
11
|
USER = os.environ.get("PGDEVKIT_TESTDB_USER", "postgres")
|
|
12
12
|
PASSWORD = os.environ.get("PGDEVKIT_TESTDB_PASSWORD", "testpwd")
|
|
13
13
|
PG_SPEED_FLAGS = ["-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
|
14
|
+
# Production connects with session timezone=UTC; the container image's own default
|
|
15
|
+
# (baked into its base OS, not something pgdevkit ever set) can differ, silently
|
|
16
|
+
# making any ::date cast of a timestamptz column disagree between test and prod for
|
|
17
|
+
# rows near local midnight. Forcing UTC here -- both as the server's own timezone GUC
|
|
18
|
+
# and as PGTZ/TZ for any client library that consults the environment instead -- keeps
|
|
19
|
+
# test parity with prod instead of depending on the host/image's locale.
|
|
20
|
+
PG_STARTUP_FLAGS = [*PG_SPEED_FLAGS, "-c", "timezone=UTC"]
|
|
14
21
|
|
|
15
22
|
|
|
16
23
|
def conninfo(dbname: str, *, connect_timeout: int | None = None) -> str:
|
|
@@ -36,8 +36,13 @@ def _create_container(client: docker.DockerClient) -> None:
|
|
|
36
36
|
environment={
|
|
37
37
|
"POSTGRES_USER": constants.USER,
|
|
38
38
|
"POSTGRES_PASSWORD": constants.PASSWORD,
|
|
39
|
+
# Belt-and-suspenders alongside the server's own -c timezone=UTC below:
|
|
40
|
+
# some client libraries/tools inside the container consult TZ/PGTZ
|
|
41
|
+
# directly instead of asking Postgres for its configured timezone.
|
|
42
|
+
"TZ": "UTC",
|
|
43
|
+
"PGTZ": "UTC",
|
|
39
44
|
},
|
|
40
|
-
command=["postgres", *constants.
|
|
45
|
+
command=["postgres", *constants.PG_STARTUP_FLAGS],
|
|
41
46
|
)
|
|
42
47
|
except docker.errors.APIError as e:
|
|
43
48
|
if getattr(e, "status_code", None) == 409 or "already in use" in str(e):
|
|
@@ -132,6 +132,19 @@ def _iter_sql_files(
|
|
|
132
132
|
):
|
|
133
133
|
"""Yield (Path, sql_content) pairs in dependency-safe execution order.
|
|
134
134
|
|
|
135
|
+
Every `permissions`-type file (see `_TYPE_ORDER`) is held back
|
|
136
|
+
unconditionally and yielded only after every other file -- including
|
|
137
|
+
anything resolved via the delayed-retry loop below -- has actually been
|
|
138
|
+
delivered. A blanket "grant ... on all tables in schema X" has no real
|
|
139
|
+
per-object dependency of its own to track, but it still must apply
|
|
140
|
+
*after* every table/view it's meant to cover; a table whose own creation
|
|
141
|
+
got deferred to the retry loop (e.g. an FK to a same-type file that
|
|
142
|
+
happens to sort later) would otherwise silently miss that grant, since
|
|
143
|
+
a schema-wide GRANT is a one-time snapshot of whatever exists when it
|
|
144
|
+
runs. Nothing ever legitimately depends on a permissions file (it
|
|
145
|
+
creates no table/view/schema of its own), so holding every one back is
|
|
146
|
+
always safe.
|
|
147
|
+
|
|
135
148
|
A file tagged for another environment (see pgdevkit.envtag) is dropped
|
|
136
149
|
during the initial file walk, before dependency ordering even sees it.
|
|
137
150
|
|
|
@@ -154,6 +167,7 @@ def _iter_sql_files(
|
|
|
154
167
|
delivered: set[str] = set()
|
|
155
168
|
delayed: list[tuple[str | None, Path, str]] = []
|
|
156
169
|
all_declared: set[str] = set()
|
|
170
|
+
permissions_files: list[tuple[Path, str]] = []
|
|
157
171
|
|
|
158
172
|
for file in sorted(files, key=lambda p: (_get_type_order(p), p.name)):
|
|
159
173
|
content = file.read_text(encoding="utf-8")
|
|
@@ -163,6 +177,9 @@ def _iter_sql_files(
|
|
|
163
177
|
sql_schemas(content, dialect), only=schemas, exclude=exclude_schemas
|
|
164
178
|
):
|
|
165
179
|
continue
|
|
180
|
+
if _get_type_order(file) == _TYPE_ORDER["permissions"]:
|
|
181
|
+
permissions_files.append((file, content))
|
|
182
|
+
continue
|
|
166
183
|
deps = _get_sql_deps(content, dialect)
|
|
167
184
|
if file.parent.name in _SCHEMA_QUALIFIED_TYPES:
|
|
168
185
|
schema = _strip_layer_prefix(file.parent.parent.name)
|
|
@@ -196,6 +213,8 @@ def _iter_sql_files(
|
|
|
196
213
|
if not progressed:
|
|
197
214
|
raise ValueError(f"Circular or missing SQL dependencies: {[f[1] for f in delayed]}")
|
|
198
215
|
|
|
216
|
+
yield from permissions_files
|
|
217
|
+
|
|
199
218
|
|
|
200
219
|
async def _insert_test_data(
|
|
201
220
|
json_file: Path,
|
|
@@ -276,6 +295,10 @@ async def apply_schema(
|
|
|
276
295
|
"""Apply every .sql file under database_dir (in dependency-safe order)
|
|
277
296
|
and seed any matching .test_data.json files. Safe to call repeatedly.
|
|
278
297
|
|
|
298
|
+
Every `permissions`-type file (see `_TYPE_ORDER`) is guaranteed to apply
|
|
299
|
+
after every table/view it's meant to cover, including anything resolved
|
|
300
|
+
via delayed-retry -- see `_iter_sql_files()`'s docstring.
|
|
301
|
+
|
|
279
302
|
A file tagged for another environment (e.g. `grants.prod.sql` when
|
|
280
303
|
`env="local_test"`) is skipped — see pgdevkit.envtag.
|
|
281
304
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- Filename ("event.sql") deliberately sorts alphabetically before its own FK
|
|
2
|
+
-- target's filename ("event_kind.sql") -- '.' (0x2E) sorts before '_' (0x5F) --
|
|
3
|
+
-- so this file's first apply attempt always fails (app.event_kind doesn't
|
|
4
|
+
-- exist yet) and it only actually gets created via _iter_sql_files()'s
|
|
5
|
+
-- delayed-retry pass, which runs after every non-delayed file has already
|
|
6
|
+
-- been attempted. See test_apply_schema_applies_permissions_after_a_delayed_table.
|
|
7
|
+
CREATE TABLE IF NOT EXISTS app.event (
|
|
8
|
+
id serial PRIMARY KEY,
|
|
9
|
+
event_kind_id int NOT NULL REFERENCES app.event_kind(id)
|
|
10
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- Roles are cluster-wide, not per-database, so this can race against another
|
|
2
|
+
-- pytest process (e.g. a second worktree) hitting the same shared testdb
|
|
3
|
+
-- container concurrently -- both can pass the IF NOT EXISTS check before
|
|
4
|
+
-- either commits. Catching duplicate_object (mirroring _ensure_database()'s
|
|
5
|
+
-- own DuplicateDatabase guard in pgdevkit/testdb/api.py) is race-safe in a
|
|
6
|
+
-- way "IF NOT EXISTS (SELECT ...) THEN CREATE" is not, since it lets
|
|
7
|
+
-- Postgres's own uniqueness check -- not a separate lookup -- be the source
|
|
8
|
+
-- of truth.
|
|
9
|
+
DO $$
|
|
10
|
+
BEGIN
|
|
11
|
+
CREATE ROLE pgdevkit_test_reader LOGIN PASSWORD 'testpwd';
|
|
12
|
+
EXCEPTION WHEN duplicate_object THEN
|
|
13
|
+
NULL;
|
|
14
|
+
END
|
|
15
|
+
$$;
|
|
16
|
+
|
|
17
|
+
GRANT USAGE ON SCHEMA app TO pgdevkit_test_reader;
|
|
18
|
+
GRANT SELECT ON ALL TABLES IN SCHEMA app TO pgdevkit_test_reader;
|
|
@@ -49,6 +49,36 @@ def test_ensure_container_skips_everything_when_skip_env_set(monkeypatch):
|
|
|
49
49
|
ensure_container() # must return without checking availability or calling the Docker API
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
class _FakeContainers:
|
|
53
|
+
def __init__(self):
|
|
54
|
+
self.run_kwargs: dict | None = None
|
|
55
|
+
|
|
56
|
+
def run(self, image, **kwargs):
|
|
57
|
+
self.run_kwargs = kwargs
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class _FakeClient:
|
|
61
|
+
def __init__(self):
|
|
62
|
+
self.containers = _FakeContainers()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_create_container_forces_utc_timezone():
|
|
66
|
+
"""Test Postgres must run with timezone=UTC regardless of the container
|
|
67
|
+
image's/host's own locale default -- production connects with session
|
|
68
|
+
timezone=UTC, so any ::date cast of a timestamptz column would otherwise
|
|
69
|
+
silently disagree between test and prod for rows near local midnight."""
|
|
70
|
+
client = _FakeClient()
|
|
71
|
+
|
|
72
|
+
_create_container(client)
|
|
73
|
+
|
|
74
|
+
assert client.containers.run_kwargs is not None
|
|
75
|
+
assert "-c" in client.containers.run_kwargs["command"]
|
|
76
|
+
timezone_flag_index = client.containers.run_kwargs["command"].index("timezone=UTC")
|
|
77
|
+
assert client.containers.run_kwargs["command"][timezone_flag_index - 1] == "-c"
|
|
78
|
+
assert client.containers.run_kwargs["environment"]["TZ"] == "UTC"
|
|
79
|
+
assert client.containers.run_kwargs["environment"]["PGTZ"] == "UTC"
|
|
80
|
+
|
|
81
|
+
|
|
52
82
|
def _admin_dsn() -> str:
|
|
53
83
|
return constants.conninfo("postgres", connect_timeout=5)
|
|
54
84
|
|
|
@@ -84,3 +114,13 @@ def test_create_container_falls_back_to_start_when_name_in_use():
|
|
|
84
114
|
with con.cursor() as cur:
|
|
85
115
|
cur.execute("SELECT 1")
|
|
86
116
|
assert cur.fetchone() == (1,)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@requires_podman
|
|
120
|
+
def test_ensure_container_runs_with_utc_timezone():
|
|
121
|
+
ensure_container()
|
|
122
|
+
|
|
123
|
+
with psycopg.connect(_admin_dsn()) as con:
|
|
124
|
+
with con.cursor() as cur:
|
|
125
|
+
cur.execute("SHOW timezone")
|
|
126
|
+
assert cur.fetchone() == ("UTC",)
|
|
@@ -4,6 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
import psycopg
|
|
6
6
|
import pytest
|
|
7
|
+
from psycopg.conninfo import make_conninfo
|
|
7
8
|
|
|
8
9
|
from pgdevkit.testdb import constants
|
|
9
10
|
from pgdevkit.testdb.container import ensure_container
|
|
@@ -97,6 +98,33 @@ async def test_apply_schema_resolves_view_to_view_dependency(schema_test_db):
|
|
|
97
98
|
assert rows == [(1, "sprocket")]
|
|
98
99
|
|
|
99
100
|
|
|
101
|
+
@requires_podman
|
|
102
|
+
async def test_apply_schema_applies_permissions_after_a_delayed_table(schema_test_db):
|
|
103
|
+
# permissions/grants.sql (a blanket "grant select on all tables in schema app")
|
|
104
|
+
# has no SQL dependencies of its own -- app.event, whose own filename
|
|
105
|
+
# ("event.sql") sorts alphabetically before its FK target's filename
|
|
106
|
+
# ("event_kind.sql"), always falls to the delayed-retry pass instead of
|
|
107
|
+
# resolving on the first attempt. Without holding permissions files back
|
|
108
|
+
# unconditionally, a blanket GRANT (a one-time snapshot of whatever exists
|
|
109
|
+
# when it runs) would apply before app.event exists and never cover it.
|
|
110
|
+
# Confirms _iter_sql_files() always yields permissions-type files last,
|
|
111
|
+
# genuinely after the whole tree (delayed retries included) has converged.
|
|
112
|
+
async with await psycopg.AsyncConnection.connect(_db_dsn(), autocommit=True) as con:
|
|
113
|
+
await apply_schema(con, FIXTURES) # must not raise
|
|
114
|
+
|
|
115
|
+
# permissions/grants.sql creates this login role itself -- a fixed,
|
|
116
|
+
# non-secret password, matching pgdevkit's own testdb container convention.
|
|
117
|
+
reader_dsn = make_conninfo(
|
|
118
|
+
host=constants.HOST, port=str(constants.PORT), dbname=TEST_DB,
|
|
119
|
+
user="pgdevkit_test_reader", password="testpwd",
|
|
120
|
+
)
|
|
121
|
+
async with await psycopg.AsyncConnection.connect(reader_dsn, autocommit=True) as con:
|
|
122
|
+
async with con.cursor() as cur:
|
|
123
|
+
await cur.execute("SELECT count(*) FROM app.event") # must not raise InsufficientPrivilege
|
|
124
|
+
(count,) = await cur.fetchone()
|
|
125
|
+
assert count == 0
|
|
126
|
+
|
|
127
|
+
|
|
100
128
|
@requires_podman
|
|
101
129
|
async def test_apply_schema_seeds_composite_enum_and_jsonb_columns(schema_test_db):
|
|
102
130
|
# gadget.mood is an enum, gadget.size a composite type, gadget.tags
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database/app/tables/gadget.test_data.json
RENAMED
|
File without changes
|
{pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database/app/tables/prod_only.prod.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database/app/tables/widget.test_data.json
RENAMED
|
File without changes
|
|
File without changes
|
{pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database/app/tables/widget_part_detail.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database/app/views/a_wrapper_view.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database_mssql/app/tables/widget.sql
RENAMED
|
File without changes
|
|
File without changes
|
{pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database_mssql/app/views/a_wrapper_view.sql
RENAMED
|
File without changes
|
{pgdevkit-0.6.0 → pgdevkit-0.6.2}/tests/testdb/fixtures/database_mssql/app/views/b_base_view.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|