pgdevkit 0.15.1__tar.gz → 0.15.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.15.1 → pgdevkit-0.15.2}/PKG-INFO +2 -2
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/README.md +1 -1
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/db/fetch.py +3 -1
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pyproject.toml +1 -1
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/skills/pgdevkit/SKILL.md +1 -1
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/db/test_fetch.py +11 -0
- pgdevkit-0.15.2/tests/db/test_query_expr_types.py +69 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/uv.lock +1 -1
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/.github/workflows/auto-release.yml +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/.github/workflows/python-publish.yml +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/.github/workflows/python-test.yml +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/.gitignore +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/.python-version +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/docs/database-layout.md +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/__init__.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/areas.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/backends/__init__.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/backends/base.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/backends/mssql.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/backends/postgres.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/cli.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/connection.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/db/__init__.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/db/complex_types.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/db/connection.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/db/crud.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/db/loader.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/db/model.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/db/mssql_crud.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/db/mssql_sql.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/db/transaction.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/dialect.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/diff.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/envtag.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/fastapi/__init__.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/fastapi/json_response.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/fetch_missing.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/introspect.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/lakebase.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/migrate.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/migrate_mssql.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/models.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/mssql_introspect.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/parser.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/schemas.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/sql_text.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/stats.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/testdb/__init__.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/testdb/_docker.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/testdb/api.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/testdb/config.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/testdb/constants.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/testdb/container.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/testdb/mssql/__init__.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/testdb/mssql/api.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/testdb/mssql/constants.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/testdb/mssql/container.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/testdb/naming.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/testdb/query.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/pgdevkit/testdb/schema.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/skills/pgdevkit/references/dynamic-sql.md +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/skills/pgdevkit/references/temporal-tables.md +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/__init__.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/_asgi.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/_granian_app.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/conftest.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/db/__init__.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/db/test_complex_types.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/db/test_connection.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/db/test_crud.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/db/test_fetch_statement_timeout.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/db/test_loader.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/db/test_mssql_crud_live.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/db/test_mssql_crud_sql.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/db/test_transaction.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/fixtures/01_schema.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/fixtures/02_types.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/fixtures/03_tables.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/fixtures/04_views.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/fixtures/05_functions.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/fixtures/06_indexes.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_areas.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_cli_compare.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_compare.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_compare_mssql_live.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_connection.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_dialect.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_diff_mssql.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_envtag.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_fetch_missing.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_fetch_missing_cli.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_lakebase.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_migrate.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_migrate_areas.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_migrate_env.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_migrate_grants.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_migrate_mssql.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_migrate_mssql_live.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_migrate_schemas.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_mssql_introspect.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_parser_areas.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_parser_mssql.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_parser_schemas.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_pg_json_cancel.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_pg_json_granian.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_pg_json_response.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_pg_json_statement_timeout.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_schemas.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_stats_cli.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/test_stats_mssql_live.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/__init__.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/conftest.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/migrations/001_add_gadget_note.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/tables/event.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/tables/event_kind.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/tables/gadget.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/tables/gadget.test_data.json +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/tables/prod_only.prod.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/tables/widget.init.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/tables/widget.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/tables/widget.test_data.json +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/tables/widget_part.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/tables/widget_part_detail.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/types/dimensions.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/types/mood.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/views/a_wrapper_view.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/views/b_base_view.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/permissions/grants.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/schema/app.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database_mssql/app/tables/widget.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database_mssql/app/tables/widget.test_data.json +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database_mssql/app/views/a_wrapper_view.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database_mssql/app/views/b_base_view.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database_mssql/schema/app.sql +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/test_api.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/test_api_mssql.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/test_api_mssql_live.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/test_cli.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/test_config.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/test_config_mssql.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/test_constants.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/test_container.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/test_container_mssql.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/test_docker.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/test_mssql_constants.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/test_naming.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/test_query.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/test_schema.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/test_schema_filtering.py +0 -0
- {pgdevkit-0.15.1 → pgdevkit-0.15.2}/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.15.
|
|
3
|
+
Version: 0.15.2
|
|
4
4
|
Summary: A helper for developing with Postgres
|
|
5
5
|
Requires-Python: >=3.14
|
|
6
6
|
Requires-Dist: docker>=7.1.0
|
|
@@ -568,7 +568,7 @@ for a `Response`, and the rows are *not* validated against the model.
|
|
|
568
568
|
starts and released when it ends. A `con` must stay open until the response has been sent, i.e. come from a `Depends`
|
|
569
569
|
with `yield` (FastAPI >= 0.118); `async with pool.connection() as conn: return PostgresJsonResponse(q, con=conn)`
|
|
570
570
|
closes it too early. Prefer the pool.
|
|
571
|
-
- **Query** — a literal string, a `psycopg.sql` composable, a sqlglot expression or a t-string, with values bound
|
|
571
|
+
- **Query** — a literal string, a `psycopg.sql` composable, a sqlglot expression (any `exp.Expr`, so Select, Union, Subquery and other `exp.Query` values are fine) or a t-string, with values bound
|
|
572
572
|
through `params` (`%(name)s`; a t-string carries its own values and takes no `params`). Passing only literals
|
|
573
573
|
is enforced by the type checker, **not at runtime**: the query is wrapped as a subquery, never parsed, so never
|
|
574
574
|
build it from user input. For a data-modifying CTE, or custom JSON, pass `query_produces_json=True` and return one
|
|
@@ -543,7 +543,7 @@ for a `Response`, and the rows are *not* validated against the model.
|
|
|
543
543
|
starts and released when it ends. A `con` must stay open until the response has been sent, i.e. come from a `Depends`
|
|
544
544
|
with `yield` (FastAPI >= 0.118); `async with pool.connection() as conn: return PostgresJsonResponse(q, con=conn)`
|
|
545
545
|
closes it too early. Prefer the pool.
|
|
546
|
-
- **Query** — a literal string, a `psycopg.sql` composable, a sqlglot expression or a t-string, with values bound
|
|
546
|
+
- **Query** — a literal string, a `psycopg.sql` composable, a sqlglot expression (any `exp.Expr`, so Select, Union, Subquery and other `exp.Query` values are fine) or a t-string, with values bound
|
|
547
547
|
through `params` (`%(name)s`; a t-string carries its own values and takes no `params`). Passing only literals
|
|
548
548
|
is enforced by the type checker, **not at runtime**: the query is wrapped as a subquery, never parsed, so never
|
|
549
549
|
build it from user input. For a data-modifying CTE, or custom JSON, pass `query_produces_json=True` and return one
|
|
@@ -28,7 +28,9 @@ QueryParams = Mapping[str, Any] | Sequence[Any] | None
|
|
|
28
28
|
# The only things a query may be: a literal string (e.g. from `SqlLoader.load_sql()`), a sqlglot
|
|
29
29
|
# expression, or psycopg's own safe-composition types (`psycopg.sql` / a t-string). A plain `str`
|
|
30
30
|
# is rejected by the type checker on purpose, so user-controlled text can't be concatenated in.
|
|
31
|
-
type
|
|
31
|
+
# The sqlglot type is `exp.Expr` (any query: Select, Union, Subquery...), not `exp.Expression`: in
|
|
32
|
+
# sqlglot 30 `exp.Query` is an `exp.Expr` but not an `exp.Expression` (issue #56), so don't narrow it back.
|
|
33
|
+
type SqlQueryNoTemplate = LiteralString | exp.Expr | Composable
|
|
32
34
|
type SqlQuery = SqlQueryNoTemplate | Template
|
|
33
35
|
|
|
34
36
|
|
|
@@ -234,7 +234,7 @@ deleted = await execute("DELETE FROM sessions WHERE user_id = %(id)s", {"id": 1}
|
|
|
234
234
|
- Passing `con=` runs on *your* connection: `fetch_all` never commits, closes or releases it. Without it, a connection is borrowed from `pool=` (or the `set_default_pool()` pool) and released afterwards.
|
|
235
235
|
- `statement_timeout=<seconds>` (optional) lets Postgres abort the query after that long, raising `QueryCanceled`; it is applied with `SET LOCAL` semantics for this call only (a borrowed `con` gets its previous value back). A plain value *replaces* the connection's own setting in both directions, so a per-call 900 s overrides a database-level safety net of 600 s; `statement_timeout=at_most(120)` (`from pgdevkit.db import at_most`) never loosens it: the effective timeout is `min(current, 120)`.
|
|
236
236
|
- `cancel=` takes an `asyncio.Event`: set it (e.g. when the HTTP client disconnects) and the running query is cancelled on the server, raising `psycopg.errors.QueryCanceled`. If the awaiting task is cancelled instead, the server-side query is cancelled too and `CancelledError` propagates as usual (not `QueryCanceled`), so no query keeps running unattended. Don't use `cancel=` on a `con` that concurrent tasks share (it aborts whatever statement is running on it); if the cancel request itself can't be delivered, the connection is closed. After `QueryCanceled` a borrowed `con` is usable again once you `await con.rollback()` (autocommit connections need nothing). To stream a whole result as JSON from FastAPI (it wires the disconnect itself) see `PostgresJsonResponse` below.
|
|
237
|
-
- `query` is typed `SqlQuery`: a literal string (`SqlLoader.load_sql()`), a sqlglot expression, a `psycopg.sql` composable or a t-string. A plain `str` fails type checking on purpose, so don't build SQL with f-strings/concatenation. A sqlglot expression is only as safe as the strings it was built from (its builders parse plain strings as SQL): pass user values as `exp.Placeholder` + `params`, never as literals/raw text. A literal `%` in a sqlglot expression (`LIKE 'a%'`, modulo) just works, with or without `params`; in a plain string or `psycopg.sql` composable it is psycopg's rule: write `%%` when you pass `params`. A t-string carries its own values, so don't also pass `params`.
|
|
237
|
+
- `query` is typed `SqlQuery`: a literal string (`SqlLoader.load_sql()`), a sqlglot expression (any `exp.Expr`, so Select, Union, Subquery and other `exp.Query` values are fine), a `psycopg.sql` composable or a t-string. A plain `str` fails type checking on purpose, so don't build SQL with f-strings/concatenation. A sqlglot expression is only as safe as the strings it was built from (its builders parse plain strings as SQL): pass user values as `exp.Placeholder` + `params`, never as literals/raw text. A literal `%` in a sqlglot expression (`LIKE 'a%'`, modulo) just works, with or without `params`; in a plain string or `psycopg.sql` composable it is psycopg's rule: write `%%` when you pass `params`. A t-string carries its own values, so don't also pass `params`.
|
|
238
238
|
|
|
239
239
|
### Dynamic SQL
|
|
240
240
|
|
|
@@ -120,6 +120,17 @@ async def test_accepts_sqlglot_expression_psycopg_sql_and_t_string(pool: PgPool)
|
|
|
120
120
|
assert await fetch_all(t"SELECT name FROM widget WHERE id = {widget_id}", pool=pool) == [{"name": "sprocket"}]
|
|
121
121
|
|
|
122
122
|
|
|
123
|
+
@requires_podman
|
|
124
|
+
async def test_accepts_exp_query_typed_union_and_subquery(pool: PgPool):
|
|
125
|
+
# annotated exp.Query on purpose (issue #56)
|
|
126
|
+
union: exp.Query = select("name").from_("widget").where("id = 1").union(select("name").from_("widget").where("id = 2"))
|
|
127
|
+
assert sorted(r["name"] for r in await fetch_all(union, pool=pool)) == ["cog", "sprocket"]
|
|
128
|
+
sub: exp.Query = select("name").from_(select("name").from_("widget").subquery("w")).where("name = 'cog'")
|
|
129
|
+
assert await fetch_all(sub, pool=pool) == [{"name": "cog"}]
|
|
130
|
+
assert await fetch_one(union, pool=pool) is not None
|
|
131
|
+
assert await fetch_scalar(select("count(*)").from_(union.subquery("u")), pool=pool) == 2
|
|
132
|
+
|
|
133
|
+
|
|
123
134
|
async def test_already_set_cancel_raises_before_touching_a_connection():
|
|
124
135
|
cancel = asyncio.Event()
|
|
125
136
|
cancel.set()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""sqlglot queries are typed as `exp.Expr`, so an `exp.Query` (base of Select/Union/Subquery) is accepted too.
|
|
2
|
+
|
|
3
|
+
In sqlglot 30 `exp.Query` is an `exp.Expr` but not an `exp.Expression`; the annotations used to say
|
|
4
|
+
`exp.Expression`, which a type checker such as pyright rejected for `exp.Query`-typed values (issue #56).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
from sqlglot import exp, select
|
|
14
|
+
|
|
15
|
+
from pgdevkit.db.fetch import _render
|
|
16
|
+
from pgdevkit.fastapi import PostgresJsonResponse
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _union() -> exp.Query:
|
|
20
|
+
return select("1 AS a").union(select("2 AS a"))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _subquery() -> exp.Query:
|
|
24
|
+
return select("a").from_(select("1 AS a").subquery("t"))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# --- runtime: no database needed for `_render` ---------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_render_union_and_subquery():
|
|
31
|
+
assert _render(_union()) == "SELECT 1 AS a UNION SELECT 2 AS a"
|
|
32
|
+
assert _render(_subquery()) == "SELECT a FROM (SELECT 1 AS a) AS t"
|
|
33
|
+
assert _render(_union(), {}) == "SELECT 1 AS a UNION SELECT 2 AS a"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_postgres_json_response_accepts_union_and_subquery():
|
|
37
|
+
for query in (_union(), _subquery()):
|
|
38
|
+
assert _render(query) in PostgresJsonResponse(query).query.as_string()
|
|
39
|
+
with pytest.raises(ValueError, match="Command"):
|
|
40
|
+
PostgresJsonResponse(select("1").union(select(exp.Command(this="SELECT", expression=" version()"))))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_render_keeps_placeholders_and_escapes_percent_inside_a_union():
|
|
44
|
+
query = (
|
|
45
|
+
select("name").from_("w").where(exp.column("id").eq(exp.Placeholder(this="id"))).where("name LIKE '50%'")
|
|
46
|
+
).union(select("'x%'"))
|
|
47
|
+
rendered = _render(query, {"id": 1})
|
|
48
|
+
assert "%(id)s" in rendered
|
|
49
|
+
assert "'50%%'" in rendered
|
|
50
|
+
assert "'x%%'" in rendered
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_render_rejects_command_and_bad_placeholder_nested_in_a_union():
|
|
54
|
+
command = exp.Command(this="SELECT", expression=" version()")
|
|
55
|
+
with pytest.raises(ValueError, match="Command"):
|
|
56
|
+
_render(select("1").union(select(command)))
|
|
57
|
+
with pytest.raises(ValueError, match="Command"):
|
|
58
|
+
_render(select("a").from_(select(command).subquery("t")))
|
|
59
|
+
bad = exp.Placeholder(this="x); DROP TABLE t;--")
|
|
60
|
+
with pytest.raises(ValueError, match="placeholder name"):
|
|
61
|
+
_render(select("1").union(select("2").where(bad)), {"x": 1})
|
|
62
|
+
with pytest.raises(ValueError, match="placeholder name"):
|
|
63
|
+
_render(select("a").from_(select("2 AS a").where(bad).subquery("t")), {"x": 1})
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_importing_pgdevkit_db_does_not_import_sqlglot():
|
|
67
|
+
code = "import sys, pgdevkit.db; sys.exit('sqlglot' in sys.modules)"
|
|
68
|
+
assert subprocess.run([sys.executable, "-c", code], check=False).returncode == 0
|
|
69
|
+
|
|
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
|
|
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.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/tables/event_kind.sql
RENAMED
|
File without changes
|
|
File without changes
|
{pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/tables/gadget.test_data.json
RENAMED
|
File without changes
|
{pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/tables/prod_only.prod.sql
RENAMED
|
File without changes
|
{pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/tables/widget.init.sql
RENAMED
|
File without changes
|
|
File without changes
|
{pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/tables/widget.test_data.json
RENAMED
|
File without changes
|
{pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/tables/widget_part.sql
RENAMED
|
File without changes
|
{pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/tables/widget_part_detail.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/views/a_wrapper_view.sql
RENAMED
|
File without changes
|
{pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database/app/views/b_base_view.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.15.1 → pgdevkit-0.15.2}/tests/testdb/fixtures/database_mssql/app/tables/widget.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.15.1 → pgdevkit-0.15.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|