@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.
- package/dist/postgres/connection.d.ts.map +1 -1
- package/dist/postgres/fake-schema-drop.d.ts +13 -0
- package/dist/postgres/fake-schema-drop.d.ts.map +1 -0
- package/dist/postgres/fake-schema-sql.d.ts +5 -0
- package/dist/postgres/fake-schema-sql.d.ts.map +1 -1
- package/dist/postgres/fake-sql.d.ts +4 -0
- package/dist/postgres/fake-sql.d.ts.map +1 -1
- package/dist/postgres/index.d.ts +2 -1
- package/dist/postgres/index.d.ts.map +1 -1
- package/dist/postgres/index.js +461 -278
- package/dist/postgres/index.js.map +19 -15
- package/dist/postgres/psql-executor.d.ts.map +1 -1
- package/dist/postgres/schema-closure-sql.d.ts +45 -0
- package/dist/postgres/schema-closure-sql.d.ts.map +1 -0
- package/dist/postgres/schema-delete.d.ts +17 -8
- package/dist/postgres/schema-delete.d.ts.map +1 -1
- package/dist/postgres/schema-drop-sql.d.ts +66 -0
- package/dist/postgres/schema-drop-sql.d.ts.map +1 -0
- package/dist/postgres/schema-errors.d.ts +19 -1
- package/dist/postgres/schema-errors.d.ts.map +1 -1
- package/dist/postgres/schema-sequence-error.d.ts +29 -0
- package/dist/postgres/schema-sequence-error.d.ts.map +1 -0
- package/dist/postgres/schema-sql.d.ts +16 -0
- package/dist/postgres/schema-sql.d.ts.map +1 -1
- package/dist/postgres/schema-test-kit.d.ts +3 -1
- package/dist/postgres/schema-test-kit.d.ts.map +1 -1
- package/dist/postgres/search-path.d.ts +38 -0
- package/dist/postgres/search-path.d.ts.map +1 -0
- package/docs/postgres-schema.md +22 -1
- package/package.json +1 -1
- package/src/postgres/connection.ts +10 -14
- package/src/postgres/database-bigint-serialization.test.ts +2 -2
- package/src/postgres/database-sql.ts +6 -6
- package/src/postgres/fake-grants-read.ts +3 -3
- package/src/postgres/fake-role-sql.ts +2 -2
- package/src/postgres/fake-schema-drop.ts +75 -0
- package/src/postgres/fake-schema-sql.ts +10 -1
- package/src/postgres/fake-sql.ts +10 -3
- package/src/postgres/grants-database.test.ts +5 -5
- package/src/postgres/grants-existence.ts +7 -7
- package/src/postgres/grants-ownership.ts +5 -5
- package/src/postgres/grants-psql-lifecycle.test.ts +7 -3
- package/src/postgres/grants-read-defaults.ts +5 -5
- package/src/postgres/grants-read.ts +16 -16
- package/src/postgres/index.ts +2 -0
- package/src/postgres/psql-executor-escaping.test.ts +3 -1
- package/src/postgres/psql-executor.test.ts +11 -5
- package/src/postgres/psql-executor.ts +4 -1
- package/src/postgres/role-adopt.test.ts +3 -1
- package/src/postgres/role-membership-options.test.ts +3 -1
- package/src/postgres/role-membership-repair.test.ts +3 -3
- package/src/postgres/role-membership-sql.ts +4 -4
- package/src/postgres/role-password-transports.test.ts +4 -2
- package/src/postgres/role-sql.ts +1 -1
- package/src/postgres/schema-closure-sql.ts +108 -0
- package/src/postgres/schema-delete-proofs.test.ts +7 -7
- package/src/postgres/schema-delete.ts +88 -46
- package/src/postgres/schema-drop-2bp01.test.ts +29 -17
- package/src/postgres/schema-drop-real.test.ts +190 -0
- package/src/postgres/schema-drop-sql.ts +135 -0
- package/src/postgres/schema-drop.test.ts +241 -0
- package/src/postgres/schema-errors.ts +26 -0
- package/src/postgres/schema-handlers.test.ts +21 -14
- package/src/postgres/schema-lifecycle.test.ts +6 -60
- package/src/postgres/schema-sequence-error.ts +38 -0
- package/src/postgres/schema-socket.test.ts +20 -6
- package/src/postgres/schema-sql.ts +16 -11
- package/src/postgres/schema-takeover.test.ts +7 -4
- package/src/postgres/schema-test-kit.ts +18 -4
- package/src/postgres/search-path.test.ts +100 -0
- 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":"
|
|
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
|
-
*
|
|
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 {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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 |
|
|
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
|
|
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":"
|
|
1
|
+
{"version":3,"file":"schema-errors.d.ts","sourceRoot":"","sources":["../../src/postgres/schema-errors.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,qCAAqC,EAAE,MAAM,4BAA4B,CAAC;;;;AAExF,2FAA2F;AAC3F,qBAAa,yBAA0B,SAAQ,+BAA8C;IAC3F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED;8BAC8B;AAC9B,qBAAa,2BAA4B,SAAQ,iCAAgD;IAC/F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAM7B;CACF;;;;AAED,yFAAyF;AACzF,qBAAa,0BAA2B,SAAQ,gCAA+C;IAC7F,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF;;;;AAED,qFAAqF;AACrF,qBAAa,mBAAoB,SAAQ,yBAAwC;IAC/E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED,4FAA4F;AAC5F,qBAAa,4BAA6B,SAAQ,kCAAiD;IACjG,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAK7B;CACF;;;;AAED;;;;;GAKG;AACH,qBAAa,+BAAgC,SAAQ,qCAEnD;IACA,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAK7B;CACF;;;;AAED;;;;;;GAMG;AACH,qBAAa,uCAAwC,SAAQ,6CAE3D;IACA,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sFAAsF;IACtF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAM7B;CACF;;;;AAED;;;;yCAIyC;AACzC,qBAAa,2BAA4B,SAAQ,iCAAgD;IAC/F,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED;;;;;;;GAOG;AACH,qBAAa,kCAAmC,SAAQ,wCAEtD;IACA,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,6FAA6F;IAC7F,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAW7B;CACF;;;;AAED;2FAC2F;AAC3F,qBAAa,6BAA8B,SAAQ,mCAEjD;IACA,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAM7B;CACF;;;;AAED,gGAAgG;AAChG,qBAAa,2BAA4B,SAAQ,iCAAgD;IAC/F,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAM7B;CACF;;;;AAED,iGAAiG;AACjG,qBAAa,6BAA8B,SAAQ,mCAEjD;IACA,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAM7B;CACF;AAED,MAAM,MAAM,mBAAmB,GAC3B,2BAA2B,GAC3B,6BAA6B,GAC7B,yBAAyB,GACzB,2BAA2B,GAC3B,0BAA0B,GAC1B,mBAAmB,GACnB,4BAA4B,GAC5B,+BAA+B,GAC/B,uCAAuC,GACvC,qCAAqC,GACrC,kCAAkC,GAClC,2BAA2B,GAC3B,6BAA6B,CAAC"}
|
|
@@ -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;
|
|
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;
|
|
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"}
|
package/docs/postgres-schema.md
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
};
|