@homeflare/alchemy 0.46.0 → 0.46.1

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.
Files changed (71) hide show
  1. package/dist/postgres/connection.d.ts.map +1 -1
  2. package/dist/postgres/fake-schema-drop.d.ts +13 -0
  3. package/dist/postgres/fake-schema-drop.d.ts.map +1 -0
  4. package/dist/postgres/fake-schema-sql.d.ts +5 -0
  5. package/dist/postgres/fake-schema-sql.d.ts.map +1 -1
  6. package/dist/postgres/fake-sql.d.ts +4 -0
  7. package/dist/postgres/fake-sql.d.ts.map +1 -1
  8. package/dist/postgres/index.d.ts +2 -1
  9. package/dist/postgres/index.d.ts.map +1 -1
  10. package/dist/postgres/index.js +461 -278
  11. package/dist/postgres/index.js.map +19 -15
  12. package/dist/postgres/psql-executor.d.ts.map +1 -1
  13. package/dist/postgres/schema-closure-sql.d.ts +45 -0
  14. package/dist/postgres/schema-closure-sql.d.ts.map +1 -0
  15. package/dist/postgres/schema-delete.d.ts +17 -8
  16. package/dist/postgres/schema-delete.d.ts.map +1 -1
  17. package/dist/postgres/schema-drop-sql.d.ts +66 -0
  18. package/dist/postgres/schema-drop-sql.d.ts.map +1 -0
  19. package/dist/postgres/schema-errors.d.ts +19 -1
  20. package/dist/postgres/schema-errors.d.ts.map +1 -1
  21. package/dist/postgres/schema-sequence-error.d.ts +29 -0
  22. package/dist/postgres/schema-sequence-error.d.ts.map +1 -0
  23. package/dist/postgres/schema-sql.d.ts +16 -0
  24. package/dist/postgres/schema-sql.d.ts.map +1 -1
  25. package/dist/postgres/schema-test-kit.d.ts +3 -1
  26. package/dist/postgres/schema-test-kit.d.ts.map +1 -1
  27. package/dist/postgres/search-path.d.ts +38 -0
  28. package/dist/postgres/search-path.d.ts.map +1 -0
  29. package/docs/postgres-schema.md +22 -1
  30. package/package.json +1 -1
  31. package/src/postgres/connection.ts +10 -14
  32. package/src/postgres/database-bigint-serialization.test.ts +2 -2
  33. package/src/postgres/database-sql.ts +6 -6
  34. package/src/postgres/fake-grants-read.ts +3 -3
  35. package/src/postgres/fake-role-sql.ts +2 -2
  36. package/src/postgres/fake-schema-drop.ts +75 -0
  37. package/src/postgres/fake-schema-sql.ts +10 -1
  38. package/src/postgres/fake-sql.ts +10 -3
  39. package/src/postgres/grants-database.test.ts +5 -5
  40. package/src/postgres/grants-existence.ts +7 -7
  41. package/src/postgres/grants-ownership.ts +5 -5
  42. package/src/postgres/grants-psql-lifecycle.test.ts +7 -3
  43. package/src/postgres/grants-read-defaults.ts +5 -5
  44. package/src/postgres/grants-read.ts +16 -16
  45. package/src/postgres/index.ts +2 -0
  46. package/src/postgres/psql-executor-escaping.test.ts +3 -1
  47. package/src/postgres/psql-executor.test.ts +11 -5
  48. package/src/postgres/psql-executor.ts +4 -1
  49. package/src/postgres/role-adopt.test.ts +3 -1
  50. package/src/postgres/role-membership-options.test.ts +3 -1
  51. package/src/postgres/role-membership-repair.test.ts +3 -3
  52. package/src/postgres/role-membership-sql.ts +4 -4
  53. package/src/postgres/role-password-transports.test.ts +4 -2
  54. package/src/postgres/role-sql.ts +1 -1
  55. package/src/postgres/schema-closure-sql.ts +108 -0
  56. package/src/postgres/schema-delete-proofs.test.ts +7 -7
  57. package/src/postgres/schema-delete.ts +88 -46
  58. package/src/postgres/schema-drop-2bp01.test.ts +29 -17
  59. package/src/postgres/schema-drop-real.test.ts +190 -0
  60. package/src/postgres/schema-drop-sql.ts +135 -0
  61. package/src/postgres/schema-drop.test.ts +241 -0
  62. package/src/postgres/schema-errors.ts +26 -0
  63. package/src/postgres/schema-handlers.test.ts +21 -14
  64. package/src/postgres/schema-lifecycle.test.ts +6 -60
  65. package/src/postgres/schema-sequence-error.ts +38 -0
  66. package/src/postgres/schema-socket.test.ts +20 -6
  67. package/src/postgres/schema-sql.ts +16 -11
  68. package/src/postgres/schema-takeover.test.ts +7 -4
  69. package/src/postgres/schema-test-kit.ts +18 -4
  70. package/src/postgres/search-path.test.ts +100 -0
  71. package/src/postgres/search-path.ts +62 -0
@@ -1 +1 @@
1
- {"version":3,"file":"psql-executor.d.ts","sourceRoot":"","sources":["../../src/postgres/psql-executor.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,KAAK,UAAU,EAAsB,MAAM,mBAAmB,CAAC;AAExE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;yDAEyD;AACzD,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE;IAC9B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAE1B,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,yFAAyF;AACzF,eAAO,MAAM,YAAY,QAAS,MAAM,UAAU,aAAa,CAAC,OAAO,CAAC,KAAG,MASnE,CAAC;AAgBT;;8EAE8E;AAC9E,eAAO,MAAM,sBAAsB,SAAU,MAAM,KAAG,MAInD,CAAC;AAuDJ,eAAO,MAAM,gBAAgB,QAAS,UAAU,UAAU,UAAU,KAAG,UAkCrE,CAAC"}
1
+ {"version":3,"file":"psql-executor.d.ts","sourceRoot":"","sources":["../../src/postgres/psql-executor.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAE,KAAK,UAAU,EAAsB,MAAM,mBAAmB,CAAC;AAGxE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;yDAEyD;AACzD,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE;IAC9B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAE1B,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,yFAAyF;AACzF,eAAO,MAAM,YAAY,QAAS,MAAM,UAAU,aAAa,CAAC,OAAO,CAAC,KAAG,MASnE,CAAC;AAgBT;;8EAE8E;AAC9E,eAAO,MAAM,sBAAsB,SAAU,MAAM,KAAG,MAInD,CAAC;AAwDJ,eAAO,MAAM,gBAAgB,QAAS,UAAU,UAAU,UAAU,KAAG,UAkCrE,CAAC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The dependency closure `DROP SCHEMA … CASCADE` would walk, and the count of what it would take
3
+ * that does NOT live in the schema being dropped (`schema-drop-sql.ts` refuses when it is > 0).
4
+ *
5
+ * ⛔ THE CLOSURE IS DERIVED FROM THE NAMESPACE OBJECT, NOT FROM A LIST OF CLASSES. Until
6
+ * 2026-10-06 the scan started from four catalogs (`pg_class`, `pg_type`, `pg_proc`,
7
+ * `pg_operator`), so `a.c` (a collation) used by `b.t.x` was invisible and the cascade reached
8
+ * into `b.t.x` (Codex read of PR 357). Now the walk starts at `(pg_namespace, v_ns)` and follows
9
+ * EVERY `pg_depend` row whose referenced object is already inside — the same edges
10
+ * `findDependentObjects` (`dependency.c`) follows — so any object class `pg_depend` can name
11
+ * (collation, conversion, opclass, opfamily, text-search, statistics, extension members, …)
12
+ * is reached with no catalog enumerated to find it.
13
+ * ★ EVERY `deptype` FOLLOWS (`pg_depend`, catalogs.sgml); they differ only in attribution:
14
+ * n NORMAL cascade drops the dependent → followed; foreign if it lives elsewhere
15
+ * a AUTO dropped with any drop → followed; foreign if it lives elsewhere
16
+ * (an owned sequence in another schema is dropped with its table)
17
+ * i INTERNAL a part of the referenced object → followed; attributed to its owner (below)
18
+ * P / S partition primary / secondary → followed; a partition in another
19
+ * schema is dropped with its parent, so it is foreign
20
+ * e EXTENSION member of an extension → followed; the extension's members are
21
+ * dropped with it, wherever they live
22
+ * x AUTO-EXT auto, and dropped with the extension → followed like `a`
23
+ * p PIN system object, `refobjid = 0` → never a referenced row, never joins
24
+ * ★ "INSIDE" is a home-schema test per class. TOAST is the case the old scan got wrong: a text
25
+ * table's TOAST relation (and its index) lives in `pg_toast` and depends internally on the
26
+ * table, so it is attributed to the table that owns it via `reltoastrelid` — otherwise every
27
+ * cascade of a schema with a text column was refused. Constraints, triggers, rules, policies
28
+ * and defaults have no schema of their own and take their table's.
29
+ * ⚠️ A CLASS THIS FILE DOES NOT KNOW (a cast, a foreign server, a language, an event trigger)
30
+ * resolves to NULL, which is NOT inside: it counts as foreign. Unknown fails CLOSED. The walk
31
+ * stops at a foreign object — one is enough to refuse.
32
+ * ★ A dependent created AFTER this scan is held off by the caller's locks (`schema-drop-sql.ts`).
33
+ */
34
+ /**
35
+ * `v_count := …`: how many objects the cascade would drop outside `v_ns`, PLUS how many objects
36
+ * INSIDE it hold an `e` or `i` edge to an object outside the closure.
37
+ * ⛔ THE SECOND TERM IS THE OWNERSHIP ESCAPE (Codex read of PR 357, 2026-10-06). An inside table
38
+ * added to a foreign extension (`ALTER EXTENSION x ADD TABLE a.t`) has an OUTGOING `e` edge; the
39
+ * walk only follows edges INTO the closure, so it saw nothing, while `findDependentObjects`
40
+ * redirects the deletion of an extension member / internal part to its OWNER and drops that
41
+ * owner's other objects too. Any outgoing `e` or `i` edge leaving the closure counts as foreign:
42
+ * refuse, never follow-and-classify (the simplest correct answer fails closed).
43
+ */
44
+ export declare const OUTSIDE_DEPENDENTS_SQL: string;
45
+ //# sourceMappingURL=schema-closure-sql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-closure-sql.d.ts","sourceRoot":"","sources":["../../src/postgres/schema-closure-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAkDH;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,QAe7B,CAAC"}
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `Postgres.Schema`'s drop path: prove the connection, prove the schema is still ours, then
3
- * the guarded `DROP SCHEMA`.
3
+ * ONE atomic statement that re-proves it under a lock and drops (`schema-drop-sql.ts`).
4
4
  *
5
5
  * ⛔ THE ENGINE DOES NOT RE-READ BEFORE A DELETE THAT HAS STATE (alchemy `Apply.ts`). When
6
6
  * persisted attributes exist, `delete` receives the LAST asserted `oid` + `owner` and no
@@ -10,19 +10,28 @@
10
10
  * row that no longer matches the persisted proof fails with
11
11
  * `PostgresSchemaDeleteForeignRefused` (`schema-assert.ts#deleteForeignRefusal`) before any
12
12
  * `DROP` is issued.
13
+ * ⛔ THE RE-READ ALONE IS A WINDOW. It is a separate autocommit statement (a separate `psql` on
14
+ * the runner transport), so the schema can change between the read and the drop. The drop
15
+ * therefore carries the proven `oid` + `owner` INTO one `DO` block that locks, re-verifies
16
+ * them, checks emptiness or cross-schema dependents, and drops in one transaction. This
17
+ * file's read is the typed, readable refusal; the block is the guarantee.
13
18
  */
14
19
  import * as Effect from 'effect/Effect';
15
20
  import type { SqlError } from 'effect/unstable/sql/SqlError';
16
21
  import type { PgExecutor } from './database-sql.ts';
17
22
  import type { PostgresSchemaAttributes, PostgresSchemaProps } from './schema-attrs.ts';
18
- import { type PostgresSchemaDeleteForeignRefused, PostgresSchemaDropNotEmptyError, type PostgresSchemaWrongDatabase } from './schema-errors.ts';
19
- /** Prove the connected database, then the guarded drop — the drop core on its own, for the
20
- * tests that drive it directly. */
21
- export declare const dropWithClient: (pg: PgExecutor, props: PostgresSchemaProps) => Effect.Effect<void, PostgresSchemaWrongDatabase | PostgresSchemaDropNotEmptyError | SqlError>;
23
+ import { PostgresSchemaCascadeCrossSchemaRefused, PostgresSchemaDeleteForeignRefused, PostgresSchemaDropNotEmptyError, type PostgresSchemaWrongDatabase } from './schema-errors.ts';
24
+ import { PostgresSchemaCascadeSequencesRefused } from './schema-sequence-error.ts';
25
+ type DropError = PostgresSchemaDropNotEmptyError | PostgresSchemaCascadeCrossSchemaRefused | PostgresSchemaCascadeSequencesRefused | PostgresSchemaDeleteForeignRefused | SqlError;
26
+ /** Prove the connected database, then the guarded drop of whatever row is live now — the drop
27
+ * core on its own, for the tests that drive it directly. No persisted proof is consulted: the
28
+ * row read here is the row the atomic drop re-verifies. */
29
+ export declare const dropWithClient: (pg: PgExecutor, props: PostgresSchemaProps) => Effect.Effect<void, PostgresSchemaWrongDatabase | DropError>;
22
30
  /**
23
31
  * The core of `delete`: re-read first (see the file header), refuse a schema the persisted
24
- * proof cannot vouch for, then the guarded drop. `output` is the persisted state the engine
25
- * hands over — `undefined` is no proof and refuses too.
32
+ * proof cannot vouch for, then the atomic guarded drop. `output` is the persisted state the
33
+ * engine hands over — `undefined` is no proof and refuses too.
26
34
  */
27
- export declare const deleteWithClient: (pg: PgExecutor, props: PostgresSchemaProps, output: PostgresSchemaAttributes | undefined) => Effect.Effect<void, PostgresSchemaWrongDatabase | PostgresSchemaDeleteForeignRefused | PostgresSchemaDropNotEmptyError | SqlError>;
35
+ export declare const deleteWithClient: (pg: PgExecutor, props: PostgresSchemaProps, output: PostgresSchemaAttributes | undefined) => Effect.Effect<void, PostgresSchemaWrongDatabase | DropError>;
36
+ export {};
28
37
  //# sourceMappingURL=schema-delete.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema-delete.d.ts","sourceRoot":"","sources":["../../src/postgres/schema-delete.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAEvF,OAAO,EACL,KAAK,kCAAkC,EACvC,+BAA+B,EAC/B,KAAK,2BAA2B,EACjC,MAAM,oBAAoB,CAAC;AAuC5B;mCACmC;AACnC,eAAO,MAAM,cAAc,OACrB,UAAU,SACP,mBAAmB,KACzB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,2BAA2B,GAAG,+BAA+B,GAAG,QAAQ,CAI3F,CAAC;AAEL;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,OACvB,UAAU,SACP,mBAAmB,UAClB,wBAAwB,GAAG,SAAS,KAC3C,MAAM,CAAC,MAAM,CACd,IAAI,EACF,2BAA2B,GAC3B,kCAAkC,GAClC,+BAA+B,GAC/B,QAAQ,CASR,CAAC"}
1
+ {"version":3,"file":"schema-delete.d.ts","sourceRoot":"","sources":["../../src/postgres/schema-delete.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAEvF,OAAO,EACL,uCAAuC,EACvC,kCAAkC,EAClC,+BAA+B,EAC/B,KAAK,2BAA2B,EACjC,MAAM,oBAAoB,CAAC;AAU5B,OAAO,EAAE,qCAAqC,EAAkB,MAAM,4BAA4B,CAAC;AAGnG,KAAK,SAAS,GACV,+BAA+B,GAC/B,uCAAuC,GACvC,qCAAqC,GACrC,kCAAkC,GAClC,QAAQ,CAAC;AA4Db;;2DAE2D;AAC3D,eAAO,MAAM,cAAc,OACrB,UAAU,SACP,mBAAmB,KACzB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,2BAA2B,GAAG,SAAS,CAM1D,CAAC;AAEL;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,OACvB,UAAU,SACP,mBAAmB,UAClB,wBAAwB,GAAG,SAAS,KAC3C,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,2BAA2B,GAAG,SAAS,CAQ1D,CAAC"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The atomic `DROP SCHEMA` `Postgres.Schema` issues: ONE `DO` statement that takes a lock,
3
+ * re-proves the schema inside the same transaction, refuses what must not be dropped, and only
4
+ * then drops.
5
+ *
6
+ * ⛔ PROOF AND DROP ARE ONE STATEMENT, ONE TRANSACTION. Before 2026-10-02 the re-read, the
7
+ * emptiness check and the `DROP` were three autocommit statements — three `psql` processes on
8
+ * the runner transport — so an out-of-band drop-and-recreate between them was dropped by name,
9
+ * `CASCADE` included (Opus read of PR 334). A `DO` block is atomic on both transports and needs
10
+ * no `BEGIN`/`COMMIT` the runner would have to thread through.
11
+ * ⛔ THE LOCK MUST CONFLICT WITH DDL, NOT ONLY WITH OTHER RUNS OF THIS PROVIDER. An advisory lock
12
+ * (`pg_advisory_xact_lock`) is cooperative: an ordinary `DROP SCHEMA` + `CREATE SCHEMA` ignores
13
+ * it, and the by-name `DROP` below would then drop the REPLACEMENT, `CASCADE` included (Codex
14
+ * read of PR 357, 2026-10-06). So, BEFORE the re-verify and held to the end of the transaction:
15
+ * (1) `COMMENT ON SCHEMA <name> IS <its current comment>` — `CommentObject` resolves the schema
16
+ * through `get_object_address(…, ShareUpdateExclusiveLock)`, which locks the namespace OBJECT
17
+ * (`LockDatabaseObject`), and `DROP SCHEMA` takes `AccessExclusiveLock` on that same object
18
+ * (`AcquireDeletionLock`), so a concurrent drop waits for us; (2) on a cascade, `LOCK TABLE …
19
+ * IN ACCESS EXCLUSIVE MODE` on every table-like relation in the schema, which holds off a
20
+ * concurrent creator of a dependent (a view or foreign key reads the table it references).
21
+ * Lock modes: PostgreSQL docs, explicit-locking.html §13.3; `COMMENT`: sql-comment.html.
22
+ * `lock_timeout` turns any wait into `55P03`, an unclassified error: the drop is REFUSED, never
23
+ * forced. A comment-less schema is rewritten as `IS NULL`, which still takes the lock.
24
+ * ⛔ RENAME TAKES NO SUCH LOCK (Codex read of PR 357, 2026-10-06): `ALTER SCHEMA … RENAME` updates
25
+ * `pg_namespace` without locking the object, so a rename-then-`CREATE SCHEMA` of the same name
26
+ * committed while we waited on a table lock would make the by-name `DROP` delete the
27
+ * REPLACEMENT. The last statement before the `DROP` re-reads the name of the verified OID and
28
+ * refuses `HF001` if it moved; only the statements between that read and the `DROP` remain.
29
+ * ⛔ SEQUENCES REFUSE THE CASCADE (`HF004`, KNOWN LIMIT; `schema-sequence-error.ts`). `LOCK TABLE`
30
+ * cannot lock a sequence, so an uncommitted `CREATE VIEW b.v AS SELECT last_value FROM a.s` is
31
+ * invisible to the closure scan, and PostgreSQL's deletion (locks first, THEN scans dependents)
32
+ * would cascade into `b.v` once it commits. A schema with serial or identity columns therefore
33
+ * cannot be cascade-dropped here: drop its contents first. The check runs before any table lock.
34
+ * ⚠️ NOT COVERED: a type,
35
+ * function or collation created in ANOTHER schema to use ours has no relation to wait on. The
36
+ * advisory lock stays: it serialises runs of this provider, which is cheap and correct.
37
+ * ★ THE VALUES ARE LITERALS INSIDE THE BODY, AND THE BODY IS ONE `E'…'` LITERAL. A `DO` body
38
+ * takes no bind parameters, and a dollar-quote tag could be forged by a schema NAME containing
39
+ * it; `quoteStringLiteral` doubles backslashes and quotes, so nesting it twice is exact.
40
+ * The `DROP` text inside is `buildDropSchemaSql` run through `EXECUTE`, so the quoting rule is
41
+ * the same single one.
42
+ * ★ REFUSALS ARE CUSTOM SQLSTATEs, read from `reason.cause.code` exactly like `2BP01`
43
+ * (`schema-sql.ts#isDependentObjectsError`): `HF001` identity changed, `HF002` not empty,
44
+ * `HF003` dependents in another schema, `HF004` a cascade over a schema holding a sequence. Class `HF` is not `42`, so both transports wrap it as
45
+ * `UnknownError` with the raw code. The `HF003` message carries a COUNT only: another owner's
46
+ * object names must not reach this stack's logs.
47
+ */
48
+ import type { SqlError } from 'effect/unstable/sql/SqlError';
49
+ export declare const IDENTITY_CHANGED = "HF001";
50
+ export declare const NOT_EMPTY = "HF002";
51
+ export declare const CROSS_SCHEMA_DEPENDENTS = "HF003";
52
+ export declare const CASCADE_SEQUENCES = "HF004";
53
+ export interface AtomicDrop {
54
+ readonly name: string;
55
+ readonly oid: number;
56
+ readonly owner: string;
57
+ readonly cascade: boolean;
58
+ }
59
+ /** The whole atomic drop: lock → re-verify → refuse or drop. Pure text, no client. */
60
+ export declare const buildAtomicDropSql: (drop: AtomicDrop) => string;
61
+ /** The server's SQLSTATE on a failed statement, whichever transport raised it. */
62
+ export declare const sqlStateOf: (error: SqlError) => string | undefined;
63
+ /** The dependents COUNT out of an `HF003` error. An unreadable message still refuses: it
64
+ * answers 1, never 0 — the refusal is the fail-closed answer. */
65
+ export declare const dependentsCount: (error: SqlError) => number;
66
+ //# sourceMappingURL=schema-drop-sql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-drop-sql.d.ts","sourceRoot":"","sources":["../../src/postgres/schema-drop-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAK7D,eAAO,MAAM,gBAAgB,UAAU,CAAC;AACxC,eAAO,MAAM,SAAS,UAAU,CAAC;AACjC,eAAO,MAAM,uBAAuB,UAAU,CAAC;AAC/C,eAAO,MAAM,iBAAiB,UAAU,CAAC;AAKzC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,sFAAsF;AACtF,eAAO,MAAM,kBAAkB,SAAU,UAAU,KAAG,MAiDrD,CAAC;AAEF,kFAAkF;AAClF,eAAO,MAAM,UAAU,UAAW,QAAQ,KAAG,MAAM,GAAG,SAKrD,CAAC;AAEF;iEACiE;AACjE,eAAO,MAAM,eAAe,UAAW,QAAQ,KAAG,MAKjD,CAAC"}
@@ -1,3 +1,4 @@
1
+ import type { PostgresSchemaCascadeSequencesRefused } from './schema-sequence-error.ts';
1
2
  declare const PostgresSchemaNameRefused_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
2
3
  readonly _tag: "PostgresSchemaNameRefused";
3
4
  } & Readonly<A>;
@@ -65,6 +66,23 @@ export declare class PostgresSchemaDropNotEmptyError extends PostgresSchemaDropN
65
66
  }> {
66
67
  get message(): string;
67
68
  }
69
+ declare const PostgresSchemaCascadeCrossSchemaRefused_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
70
+ readonly _tag: "PostgresSchemaCascadeCrossSchemaRefused";
71
+ } & Readonly<A>;
72
+ /**
73
+ * `cascade: true` refused: `DROP SCHEMA … CASCADE` would also drop objects that live in OTHER
74
+ * schemas because they depend on something in this one (a view, a foreign key or a trigger
75
+ * another stack owns). The refusal carries the COUNT only — never an object name, so the error
76
+ * cannot disclose another owner's catalog. Nothing was dropped: move or drop those dependents
77
+ * deliberately, or empty this schema by hand and declare `cascade: false`.
78
+ */
79
+ export declare class PostgresSchemaCascadeCrossSchemaRefused extends PostgresSchemaCascadeCrossSchemaRefused_base<{
80
+ readonly schema: string;
81
+ /** How many dependent objects live outside this schema (at least one when raised). */
82
+ readonly dependents: number;
83
+ }> {
84
+ get message(): string;
85
+ }
68
86
  declare const PostgresSchemaWrongDatabase_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
69
87
  readonly _tag: "PostgresSchemaWrongDatabase";
70
88
  } & Readonly<A>;
@@ -134,6 +152,6 @@ export declare class PostgresSchemaIdentityRefused extends PostgresSchemaIdentit
134
152
  }> {
135
153
  get message(): string;
136
154
  }
137
- export type PostgresSchemaError = PostgresSchemaExistsRefused | PostgresSchemaIdentityRefused | PostgresSchemaNameRefused | PostgresSchemaRenameRefused | PostgresSchemaOwnerMissing | PostgresSchemaDrift | PostgresSchemaCreateVanished | PostgresSchemaDropNotEmptyError | PostgresSchemaDeleteForeignRefused | PostgresSchemaWrongDatabase | PostgresSchemaDatabaseRefused;
155
+ export type PostgresSchemaError = PostgresSchemaExistsRefused | PostgresSchemaIdentityRefused | PostgresSchemaNameRefused | PostgresSchemaRenameRefused | PostgresSchemaOwnerMissing | PostgresSchemaDrift | PostgresSchemaCreateVanished | PostgresSchemaDropNotEmptyError | PostgresSchemaCascadeCrossSchemaRefused | PostgresSchemaCascadeSequencesRefused | PostgresSchemaDeleteForeignRefused | PostgresSchemaWrongDatabase | PostgresSchemaDatabaseRefused;
138
156
  export {};
139
157
  //# sourceMappingURL=schema-errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema-errors.d.ts","sourceRoot":"","sources":["../../src/postgres/schema-errors.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema-errors.d.ts","sourceRoot":"","sources":["../../src/postgres/schema-errors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ declare const PostgresSchemaCascadeSequencesRefused_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
2
+ readonly _tag: "PostgresSchemaCascadeSequencesRefused";
3
+ } & Readonly<A>;
4
+ /**
5
+ * `cascade: true` on a schema that holds a SEQUENCE is refused (`HF004`, `schema-drop-sql.ts`).
6
+ *
7
+ * ⛔ A sequence cannot be locked by `LOCK TABLE`, so the drop's closure proof has a hole there: a
8
+ * concurrent, uncommitted `CREATE VIEW other.v AS SELECT last_value FROM ours.seq` is invisible
9
+ * to the scan, and when it commits while the drop waits on the sequence, PostgreSQL's deletion
10
+ * cascades into `other.v` (Codex read of PR 357, 2026-10-06). Serial and identity columns each
11
+ * own a sequence, so such a schema cannot be cascade-dropped by this provider.
12
+ * The message carries a COUNT only. Nothing was dropped: drop the schema's contents deliberately
13
+ * (the sequences included), then declare `cascade: false`.
14
+ */
15
+ export declare class PostgresSchemaCascadeSequencesRefused extends PostgresSchemaCascadeSequencesRefused_base<{
16
+ readonly schema: string;
17
+ /** How many sequences the schema holds (at least one when raised). */
18
+ readonly sequences: number;
19
+ }> {
20
+ get message(): string;
21
+ }
22
+ /** The sequence COUNT out of an `HF004` error; unreadable still refuses (1, never 0). */
23
+ export declare const sequencesCount: (error: {
24
+ readonly reason: {
25
+ readonly cause?: unknown;
26
+ };
27
+ }) => number;
28
+ export {};
29
+ //# sourceMappingURL=schema-sequence-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-sequence-error.d.ts","sourceRoot":"","sources":["../../src/postgres/schema-sequence-error.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;GAUG;AACH,qBAAa,qCAAsC,SAAQ,2CAEzD;IACA,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;AAED,yFAAyF;AACzF,eAAO,MAAM,cAAc,UAAW;IACpC,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC/C,KAAG,MAIH,CAAC"}
@@ -27,6 +27,22 @@ export declare const selectSchema: (pg: PgExecutor, name: string) => Effect.Effe
27
27
  * family connection points at. */
28
28
  export declare const currentDatabase: (pg: PgExecutor) => Effect.Effect<string, SqlError>;
29
29
  export declare const currentUser: (pg: PgExecutor) => Effect.Effect<string, SqlError>;
30
+ /**
31
+ * "Empty" means no object of ANY kind the schema owns. Measured object kinds live in four
32
+ * catalogs: relations (tables, views, indexes, sequences — `pg_class`), functions and
33
+ * aggregates (`pg_proc`), types, domains and enums (`pg_type`), operators (`pg_operator`). A
34
+ * kind without a dedicated catalog (an extension's `extnamespace`, a collation) can still pass
35
+ * this check — and then the plain `DROP SCHEMA` answers `2BP01`, which `dropWithClient`
36
+ * classifies as the same typed refusal.
37
+ *
38
+ * ⚠️ THE FAKE (`fake-schema-sql.ts`) ROUTES THIS STATEMENT BY `AS empty` + `pg_class` — keep both
39
+ * markers in any rewrite, and keep this check matched BEFORE the plain `pg_namespace` branch
40
+ * (this SQL contains `FROM pg_catalog.pg_namespace` too, inside its `WITH`).
41
+ */
42
+ /** The four-catalog emptiness predicate over a namespace oid expression — shared by this read
43
+ * and by the atomic drop's in-transaction check (`schema-drop-sql.ts`), so the two cannot
44
+ * disagree about what "empty" means. */
45
+ export declare const emptyPredicate: (ns: string) => string;
30
46
  /** `true` when the schema holds none of the four object catalogs above (used before a
31
47
  * non-cascade `DROP SCHEMA`). */
32
48
  export declare const schemaIsEmpty: (pg: PgExecutor, name: string) => Effect.Effect<boolean, SqlError>;
@@ -1 +1 @@
1
- {"version":3,"file":"schema-sql.d.ts","sourceRoot":"","sources":["../../src/postgres/schema-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAEvF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,2FAA2F;AAC3F,eAAO,MAAM,oBAAoB,UAAW,mBAAmB,KAAG,MAG9D,CAAC;AAEL,mFAAmF;AACnF,eAAO,MAAM,qBAAqB,SAAU,MAAM,WAAW,MAAM,KAAG,MACK,CAAC;AAE5E,2FAA2F;AAC3F,eAAO,MAAM,kBAAkB,SAAU,MAAM,WAAW,OAAO,KAAG,MACK,CAAC;AAW1E;4FAC4F;AAC5F,eAAO,MAAM,YAAY,OACnB,UAAU,QACR,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,wBAAwB,GAAG,SAAS,EAAE,QAAQ,CACgC,CAAC;AAIhG;;kCAEkC;AAClC,eAAO,MAAM,eAAe,OAAQ,UAAU,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAM5E,CAAC;AAUJ,eAAO,MAAM,WAAW,OAAQ,UAAU,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAKxE,CAAC;AAyBJ;iCACiC;AACjC,eAAO,MAAM,aAAa,OAAQ,UAAU,QAAQ,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,CAIzF,CAAC;AAEJ;;;;;;;;6FAQ6F;AAC7F,eAAO,MAAM,uBAAuB,UAAW,QAAQ,KAAG,OAKzD,CAAC;AAEF,0FAA0F;AAC1F,eAAO,MAAM,sBAAsB,UAAW,QAAQ,KAAG,OAKxD,CAAC"}
1
+ {"version":3,"file":"schema-sql.d.ts","sourceRoot":"","sources":["../../src/postgres/schema-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAEvF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,2FAA2F;AAC3F,eAAO,MAAM,oBAAoB,UAAW,mBAAmB,KAAG,MAG9D,CAAC;AAEL,mFAAmF;AACnF,eAAO,MAAM,qBAAqB,SAAU,MAAM,WAAW,MAAM,KAAG,MACK,CAAC;AAE5E,2FAA2F;AAC3F,eAAO,MAAM,kBAAkB,SAAU,MAAM,WAAW,OAAO,KAAG,MACK,CAAC;AAW1E;4FAC4F;AAC5F,eAAO,MAAM,YAAY,OACnB,UAAU,QACR,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,wBAAwB,GAAG,SAAS,EAAE,QAAQ,CACgC,CAAC;AAIhG;;kCAEkC;AAClC,eAAO,MAAM,eAAe,OAAQ,UAAU,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAM5E,CAAC;AAUJ,eAAO,MAAM,WAAW,OAAQ,UAAU,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAKxE,CAAC;AAEJ;;;;;;;;;;;GAWG;AACH;;wCAEwC;AACxC,eAAO,MAAM,cAAc,OAAQ,MAAM,KAAG,MAQxC,CAAC;AAKL;iCACiC;AACjC,eAAO,MAAM,aAAa,OAAQ,UAAU,QAAQ,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,CAIzF,CAAC;AAEJ;;;;;;;;6FAQ6F;AAC7F,eAAO,MAAM,uBAAuB,UAAW,QAAQ,KAAG,OAKzD,CAAC;AAEF,0FAA0F;AAC1F,eAAO,MAAM,sBAAsB,UAAW,QAAQ,KAAG,OAKxD,CAAC"}
@@ -37,7 +37,9 @@ export declare const router: (answers: RouteAnswers) => (stdin: string) => strin
37
37
  * `answers`. The argv recording pins the `-d` database each statement ran against — the last
38
38
  * element of the argv is the `psql -d` value (`psql-executor.ts`).
39
39
  */
40
- export declare const runnerWith: (answers: (stdin: string) => string) => {
40
+ export declare const runnerWith: (answers: (stdin: string) => string,
41
+ /** The `psql` stderr a statement fails with (exit 3), or `undefined` to answer normally. */
42
+ failOn?: (stdin: string) => string | undefined) => {
41
43
  run: PsqlRunner;
42
44
  stdins: string[];
43
45
  argvs: Array<readonly string[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"schema-test-kit.d.ts","sourceRoot":"","sources":["../../src/postgres/schema-test-kit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAEvF,eAAO,MAAM,EAAE,WAAY,MAAM;;;;EAAqD,CAAC;AAEvF,6FAA6F;AAC7F,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,+FAA+F;IAC/F,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,wFAAwF;IACxF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,8FAA8F;AAC9F,eAAO,MAAM,KAAK,UAAW,MAAM,WAAW,YAAY,KAAG,MAAM,GAAG,SAMrE,CAAC;AAEF,yFAAyF;AACzF,eAAO,MAAM,MAAM,YAAa,YAAY,aAAa,MAAM,WAAgC,CAAC;AAEhG;;;;GAIG;AACH,eAAO,MAAM,UAAU,YACZ,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,KACjC;IAAE,GAAG,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC,SAAS,MAAM,EAAE,CAAC,CAAA;CAYtE,CAAC;AAEF;;mDAEmD;AACnD,eAAO,MAAM,UAAU,SAAU,mBAAmB,UAAU,wBAAwB;;;;;;aAM9D,KAAK;cACX,KAAK;CACrB,CAAC;AAEH,2FAA2F;AAC3F,eAAO,MAAM,QAAQ,SAAU,mBAAmB;;;;;;aAM1B,KAAK;cACX,KAAK;CACrB,CAAC"}
1
+ {"version":3,"file":"schema-test-kit.d.ts","sourceRoot":"","sources":["../../src/postgres/schema-test-kit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,OAAO,KAAK,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAEvF,eAAO,MAAM,EAAE,WAAY,MAAM;;;;EAAqD,CAAC;AAEvF,6FAA6F;AAC7F,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,+FAA+F;IAC/F,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,wFAAwF;IACxF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,8FAA8F;AAC9F,eAAO,MAAM,KAAK,UAAW,MAAM,WAAW,YAAY,KAAG,MAAM,GAAG,SASrE,CAAC;AAEF,yFAAyF;AACzF,eAAO,MAAM,MAAM,YAAa,YAAY,aAAa,MAAM,WAAgC,CAAC;AAEhG;;;;GAIG;AACH,eAAO,MAAM,UAAU,YACZ,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM;AAClC,4FAA4F;SACpF,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,KAC5C;IAAE,GAAG,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC,SAAS,MAAM,EAAE,CAAC,CAAA;CAoBtE,CAAC;AAEF;;mDAEmD;AACnD,eAAO,MAAM,UAAU,SAAU,mBAAmB,UAAU,wBAAwB;;;;;;aAM9D,KAAK;cACX,KAAK;CACrB,CAAC;AAEH,2FAA2F;AAC3F,eAAO,MAAM,QAAQ,SAAU,mBAAmB;;;;;;aAM1B,KAAK;cACX,KAAK;CACrB,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The session setup every `Postgres.*` connection runs first: `search_path = pg_catalog, pg_temp`.
3
+ *
4
+ * ⛔ AN UNPINNED SESSION READS ITS OWN CATALOGS THROUGH `search_path`. Postgres searches
5
+ * `pg_catalog` first only while the path does not NAME it (ddl.sgml 5.10.3); a role- or
6
+ * database-level `ALTER … SET search_path = public, pg_catalog` (or a path naming any schema
7
+ * another role may create in) puts a writable schema ahead of it, and a `public.pg_namespace`
8
+ * TABLE then answers the delete proof's `oid`/`owner` re-read. Found by the Opus read of PR 334
9
+ * (2026-10-02). The pin makes the order ours whatever the role, database or server says; the SQL
10
+ * builders ALSO write `pg_catalog.<name>` (`catalog-qualified.test.ts` greps for it), so no
11
+ * single layer is the whole defence.
12
+ * ★ `pg_temp` goes LAST on purpose: unnamed, it is searched FIRST for relations.
13
+ * ★ TWO TRANSPORTS, TWO MECHANISMS. The runner path prefixes the stdin script: `psql` argv is
14
+ * the consumer's to join into a remote command, and a value with a space would split. The
15
+ * socket path pins the ONE connection an operation reserves, because the driver's startup
16
+ * message carries user, database and application_name only (`PgConnection.ts`, 4.0.0-rc.115)
17
+ * and a pool hands each statement a different, unpinned connection.
18
+ */
19
+ import type { PgClient } from '@effect/sql-pg/PgClient';
20
+ import * as Effect from 'effect/Effect';
21
+ import type * as Scope from 'effect/Scope';
22
+ import type { SqlError } from 'effect/unstable/sql/SqlError';
23
+ import type { PgExecutor } from './database-sql.ts';
24
+ export declare const PIN_SEARCH_PATH_SQL = "SET search_path = pg_catalog, pg_temp";
25
+ /** The runner path's script prefix. */
26
+ export declare const PIN_SCRIPT_PREFIX = "SET search_path = pg_catalog, pg_temp;\n";
27
+ /** A runner stdin without its pin — for tests that assert the statement that follows it. */
28
+ export declare const stripPin: (stdin: string) => string;
29
+ /**
30
+ * A `PgExecutor` over ONE reserved, pinned connection, for the life of the surrounding scope.
31
+ * `unsafe` runs on it (the client routes any statement under `transactionService` to that
32
+ * connection, so spans and error classification stay the driver's); `transaction` is a plain
33
+ * `BEGIN` … `COMMIT` on it — `withTransaction` would open a SAVEPOINT, because the service is
34
+ * already provided. A failure rolls back, and the pin is re-asserted by the next operation's own
35
+ * reservation, never assumed to survive one.
36
+ */
37
+ export declare const pinnedSocketExecutor: (pg: PgClient) => Effect.Effect<PgExecutor, SqlError, Scope.Scope>;
38
+ //# sourceMappingURL=search-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-path.d.ts","sourceRoot":"","sources":["../../src/postgres/search-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,eAAO,MAAM,mBAAmB,0CAA0C,CAAC;AAE3E,uCAAuC;AACvC,eAAO,MAAM,iBAAiB,6CAA8B,CAAC;AAE7D,4FAA4F;AAC5F,eAAO,MAAM,QAAQ,UAAW,MAAM,KAAG,MAC4C,CAAC;AAEtF;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,OAC3B,QAAQ,KACX,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,QAAQ,EAAE,KAAK,CAAC,KAAK,CAkB9C,CAAC"}
@@ -97,7 +97,28 @@ empty, or when `cascade: true` is declared.
97
97
  as the same typed tag. `2BP01` is class `2B`, so both transports wrap it as `UnknownError`
98
98
  with the raw code on `reason.cause.code` — the classifier reads that code, not the tag.
99
99
  - With `cascade: true` the drop is `DROP SCHEMA IF EXISTS … CASCADE` and removes the schema's
100
- objects too. The `IF EXISTS` makes delete idempotent.
100
+ objects too — and, because Postgres cascades through `pg_depend`, anything in ANOTHER schema
101
+ that depends on them (a view, a foreign key, a trigger another stack owns). So before a
102
+ cascade drop the atomic block counts those dependents and refuses with
103
+ `PostgresSchemaCascadeCrossSchemaRefused`, naming the COUNT only (never another owner's
104
+ object names). A dependent whose catalog class is not recognised counts as foreign: the check
105
+ fails closed. The `IF EXISTS` makes delete idempotent.
106
+ - Known limit: `cascade: true` over a schema that holds a sequence (every serial or identity
107
+ column owns one) is refused with `PostgresSchemaCascadeSequencesRefused` (`HF004`, count only).
108
+ A sequence cannot be locked, so a concurrent view over it could escape the dependency proof;
109
+ drop the schema's contents first, then declare `cascade: false`.
110
+ - The proof and the drop are ONE statement. The re-read, the emptiness (or dependents) check and
111
+ the `DROP` run in a single `DO` block, so on the runner transport it is one `psql`. The block
112
+ takes `pg_advisory_xact_lock(<schema oid>)`, re-verifies the persisted `oid` + `owner` inside
113
+ the transaction, and only then drops; a schema replaced after the read is
114
+ `PostgresSchemaDeleteForeignRefused`. The lock is cooperative (it serialises two runs of this
115
+ provider, not an arbitrary `CREATE TABLE`); a creator that slips in after the check is still
116
+ caught by the server's own `2BP01` on a plain drop.
117
+ - Every session pins `search_path = pg_catalog, pg_temp` (`search-path.ts`) and every SQL
118
+ builder writes `pg_catalog.<name>`, so a role that can create objects in a schema on its
119
+ path cannot forge `pg_namespace` and fake the delete proof. Runner path: the script is
120
+ prefixed with the `SET`; socket path: the one reserved connection an operation uses is pinned
121
+ first (`search-path.test.ts` greps the builders for an unqualified catalog name).
101
122
  - A `cascade` change is a real change: it answers `update` (compared against the previous
102
123
  declaration's persisted props), so the flipped value reaches state and the eventual drop
103
124
  uses it — a schema first declared `cascade: true` stops dropping with CASCADE once the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homeflare/alchemy",
3
- "version": "0.46.0",
3
+ "version": "0.46.1",
4
4
  "description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
5
5
  "homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
6
6
  "bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
@@ -20,6 +20,7 @@ import * as Layer from 'effect/Layer';
20
20
  import type { SqlError } from 'effect/unstable/sql/SqlError';
21
21
  import type { PgExecutor } from './database-sql.ts';
22
22
  import { type PsqlRunner, makePsqlExecutor } from './psql-executor.ts';
23
+ import { pinnedSocketExecutor } from './search-path.ts';
23
24
 
24
25
  /** What a stack passes to reach one cluster. `host` may be a socket directory (one beginning
25
26
  * with `/` expands to `${host}/.s.PGSQL.${port}` — measured in
@@ -109,21 +110,16 @@ export const withPg = <A, E>(
109
110
  });
110
111
  }
111
112
  const pool = yield* PostgresPool;
113
+ // ⛔ ONE RESERVED, PINNED CONNECTION FOR THE WHOLE OPERATION (`search-path.ts`): the pin is a
114
+ // session setting, so a pool checkout per statement would run most statements unpinned — and
115
+ // would autocommit CREATE before GRANT. `Effect.scoped` releases the reservation; the pool
116
+ // layer below closes the pool after.
112
117
  return yield* Effect.provide(
113
- Effect.flatMap(PgClient.PgClient, (pg) =>
114
- build(
115
- {
116
- unsafe: (sql, params) => pg.unsafe(sql, params),
117
- // One reserved connection (`SqlClient.withTransaction`). A pool checkout per
118
- // statement would autocommit CREATE before GRANT.
119
- transaction: (statements) =>
120
- pg.withTransaction(
121
- Effect.gen(function* () {
122
- for (const sql of statements) yield* pg.unsafe(sql).pipe(Effect.asVoid);
123
- }),
124
- ),
125
- },
126
- { database: target.database },
118
+ Effect.scoped(
119
+ Effect.flatMap(PgClient.PgClient, (pg) =>
120
+ Effect.flatMap(pinnedSocketExecutor(pg), (executor) =>
121
+ build(executor, { database: target.database }),
122
+ ),
127
123
  ),
128
124
  ),
129
125
  pool(target),
@@ -76,10 +76,10 @@ const wireAccuratePg = (seed?: {
76
76
  return {
77
77
  unsafe: <A extends object>(text: string, params: ReadonlyArray<unknown> = []) =>
78
78
  Effect.suspend(() => {
79
- if (text.startsWith('SELECT 1 AS present FROM pg_roles')) {
79
+ if (text.startsWith('SELECT 1 AS present FROM pg_catalog.pg_roles')) {
80
80
  return Effect.succeed([{ present: 1 }] as unknown as ReadonlyArray<A>);
81
81
  }
82
- if (text.includes('FROM pg_database')) {
82
+ if (text.includes('FROM pg_catalog.pg_database')) {
83
83
  if (created === undefined) return Effect.succeed([] as unknown as ReadonlyArray<A>);
84
84
  const row = wireAccurateRow(text, created.oid, created.connectionLimit);
85
85
  return Effect.succeed([row] as unknown as ReadonlyArray<A>);
@@ -103,7 +103,7 @@ export const isDuplicateDatabaseRace = (error: SqlError): boolean => {
103
103
  );
104
104
  };
105
105
 
106
- const ROLE_EXISTS_SQL = 'SELECT 1 AS present FROM pg_roles WHERE rolname = $1';
106
+ const ROLE_EXISTS_SQL = 'SELECT 1 AS present FROM pg_catalog.pg_roles WHERE rolname = $1';
107
107
 
108
108
  /** `OWNER` is checked against `pg_roles` before any `CREATE`, so a missing role is
109
109
  * `PostgresDatabaseOwnerMissing` at reconcile time rather than the server's own
@@ -114,7 +114,7 @@ export const roleExists = (pg: PgExecutor, role: string): Effect.Effect<boolean,
114
114
  (rows) => rows.length > 0,
115
115
  );
116
116
 
117
- const DATABASE_EXISTS_SQL = 'SELECT 1 AS present FROM pg_database WHERE datname = $1';
117
+ const DATABASE_EXISTS_SQL = 'SELECT 1 AS present FROM pg_catalog.pg_database WHERE datname = $1';
118
118
 
119
119
  /**
120
120
  * Does one database of the cluster exist? `Postgres.Schema` runs this over the FAMILY
@@ -152,8 +152,8 @@ export const databaseExists = (pg: PgExecutor, name: string): Effect.Effect<bool
152
152
  const SELECT_DATABASE_SQL = `SELECT
153
153
  d.oid AS oid,
154
154
  d.datname AS name,
155
- pg_get_userbyid(d.datdba) AS owner,
156
- pg_encoding_to_char(d.encoding) AS encoding,
155
+ pg_catalog.pg_get_userbyid(d.datdba) AS owner,
156
+ pg_catalog.pg_encoding_to_char(d.encoding) AS encoding,
157
157
  CASE d.datlocprovider
158
158
  WHEN 'b' THEN 'builtin' WHEN 'i' THEN 'icu' WHEN 'c' THEN 'libc'
159
159
  ELSE 'libc'
@@ -164,8 +164,8 @@ const SELECT_DATABASE_SQL = `SELECT
164
164
  d.datconnlimit AS "connectionLimit",
165
165
  d.datistemplate AS "isTemplate",
166
166
  t.spcname AS tablespace
167
- FROM pg_database d
168
- JOIN pg_tablespace t ON t.oid = d.dattablespace
167
+ FROM pg_catalog.pg_database d
168
+ JOIN pg_catalog.pg_tablespace t ON t.oid = d.dattablespace
169
169
  WHERE d.datname = $1`;
170
170
 
171
171
  /** The read: one bound `SELECT` on `pg_database`, joined to the two functions that turn its raw
@@ -96,17 +96,17 @@ export const answerRead = <A extends object>(
96
96
  ): Effect.Effect<ReadonlyArray<A>, SqlError> | undefined => {
97
97
  const { acl, schemas, roles, databases, tables } = model;
98
98
 
99
- if (text.startsWith('SELECT 1 AS present FROM pg_roles')) {
99
+ if (text.startsWith('SELECT 1 AS present FROM pg_catalog.pg_roles')) {
100
100
  return Effect.succeed(
101
101
  (roles.has(params[0] as string) ? [{ present: 1 }] : []) as unknown as ReadonlyArray<A>,
102
102
  );
103
103
  }
104
- if (text.startsWith('SELECT 1 AS present FROM pg_namespace')) {
104
+ if (text.startsWith('SELECT 1 AS present FROM pg_catalog.pg_namespace')) {
105
105
  return Effect.succeed(
106
106
  (schemas.has(params[0] as string) ? [{ present: 1 }] : []) as unknown as ReadonlyArray<A>,
107
107
  );
108
108
  }
109
- if (text.startsWith('SELECT 1 AS present FROM pg_database')) {
109
+ if (text.startsWith('SELECT 1 AS present FROM pg_catalog.pg_database')) {
110
110
  return Effect.succeed(
111
111
  (databases.has(params[0] as string) ? [{ present: 1 }] : []) as unknown as ReadonlyArray<A>,
112
112
  );
@@ -97,7 +97,7 @@ export const applyRoleStatement = <A extends object>(
97
97
  text: string,
98
98
  params: ReadonlyArray<unknown>,
99
99
  ): Effect.Effect<ReadonlyArray<A>, SqlError> | undefined => {
100
- if (text.includes('FROM pg_auth_members')) {
100
+ if (text.includes('FROM pg_catalog.pg_auth_members')) {
101
101
  const member = params[0] as string;
102
102
  const rows = [...state.memberships]
103
103
  .filter((key) => key.split('\0')[0] === member)
@@ -119,7 +119,7 @@ export const applyRoleStatement = <A extends object>(
119
119
  return Effect.succeed(rows as unknown as ReadonlyArray<A>);
120
120
  }
121
121
 
122
- if (text.includes('FROM pg_roles r')) {
122
+ if (text.includes('FROM pg_catalog.pg_roles r')) {
123
123
  const name = params[0] as string;
124
124
  const row = state.roleRows.get(name);
125
125
  // pg_roles answers no password value (S25): even a seeded seal is not read back — state
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The fake's reading of the atomic `DO` drop `schema-drop-sql.ts` emits.
3
+ *
4
+ * ⛔ IT PARSES ITS OWN OUTPUT (the rule `fake-sql.ts` documents): the four `DECLARE` literals are
5
+ * read back out of the generated text and the block's decisions are replayed against the
6
+ * in-memory catalog in the same order the SQL makes them — absent → return, identity, empty or
7
+ * cross-schema dependents, drop. A changed block shape fails the parse loudly.
8
+ */
9
+ import * as Effect from 'effect/Effect';
10
+ import { SqlError, UnknownError } from 'effect/unstable/sql/SqlError';
11
+ import { parseLiteral } from './fake-sql-quote.ts';
12
+ import { parseDropSchema } from './fake-sql-parse.ts';
13
+ import type { FakeSchemaState } from './fake-schema-sql.ts';
14
+ import {
15
+ CASCADE_SEQUENCES,
16
+ CROSS_SCHEMA_DEPENDENTS,
17
+ IDENTITY_CHANGED,
18
+ NOT_EMPTY,
19
+ } from './schema-drop-sql.ts';
20
+
21
+ const refusal = (code: string, message: string): SqlError =>
22
+ new SqlError({
23
+ reason: new UnknownError({
24
+ cause: Object.assign(new Error(message), { code }),
25
+ message,
26
+ operation: 'DO',
27
+ }),
28
+ });
29
+
30
+ /** `E'…'` literal at the end of a generated line, decoded. */
31
+ const literalAfter = (body: string, label: string): string => {
32
+ const match = new RegExp(`${label} [a-z]+ := (E'(?:[^']|'')*');`).exec(body);
33
+ if (match === null) throw new Error(`fake-sql: could not parse DO block ${label}: ${body}`);
34
+ return parseLiteral(match[1] as string);
35
+ };
36
+
37
+ export const applyAtomicDrop = (
38
+ state: FakeSchemaState,
39
+ text: string,
40
+ ): Effect.Effect<ReadonlyArray<never>, SqlError> => {
41
+ const body = parseLiteral(text.slice('DO '.length));
42
+ const name = literalAfter(body, 'v_name');
43
+ const owner = literalAfter(body, 'v_owner');
44
+ const oid = Number(/v_oid oid := (\d+);/.exec(body)?.[1]);
45
+ const cascade = /v_cascade boolean := (true|false);/.exec(body)?.[1] === 'true';
46
+ const drop = /EXECUTE (E'(?:[^']|'')*');/.exec(body)?.[1];
47
+ if (drop === undefined || Number.isNaN(oid)) {
48
+ throw new Error(`fake-sql: could not parse DO block: ${body}`);
49
+ }
50
+ if (parseDropSchema(parseLiteral(drop)).name !== name) {
51
+ throw new Error('fake-sql: DO block drops a different schema than it proves');
52
+ }
53
+ const row = state.schemas.get(name);
54
+ if (row === undefined) return Effect.succeed([]);
55
+ if (row.oid !== oid || row.owner !== owner) {
56
+ return Effect.fail(refusal(IDENTITY_CHANGED, 'schema identity changed'));
57
+ }
58
+ if (!cascade) {
59
+ if (state.relationsIn.has(name)) return Effect.fail(refusal(NOT_EMPTY, 'schema not empty'));
60
+ } else {
61
+ const sequences = state.sequencesIn.get(name) ?? 0;
62
+ if (sequences > 0) {
63
+ return Effect.fail(refusal(CASCADE_SEQUENCES, `cascade sequences: ${String(sequences)}`));
64
+ }
65
+ const outside = state.dependentsOutside.get(name) ?? 0;
66
+ if (outside > 0) {
67
+ return Effect.fail(
68
+ refusal(CROSS_SCHEMA_DEPENDENTS, `cross-schema dependents: ${String(outside)}`),
69
+ );
70
+ }
71
+ state.relationsIn.delete(name);
72
+ }
73
+ state.schemas.delete(name);
74
+ return Effect.succeed([]);
75
+ };