@rebasepro/server-postgres 0.23.0 → 0.24.0

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 (108) hide show
  1. package/dist/{BranchService-DGPL6G_C.js → BranchService-BRt78gfa.js} +35 -18
  2. package/dist/BranchService-BRt78gfa.js.map +1 -0
  3. package/dist/PostgresBackendDriver.d.ts +84 -12
  4. package/dist/auth/services.d.ts +72 -14
  5. package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
  6. package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
  7. package/dist/{backup-cli-24v4OlSp.js → backup-cli-DW5p9_zv.js} +2 -2
  8. package/dist/{backup-cli-24v4OlSp.js.map → backup-cli-DW5p9_zv.js.map} +1 -1
  9. package/dist/{backup-service-HQ9GC4tN.js → backup-service-EwcDVG-8.js} +7 -9
  10. package/dist/{backup-service-HQ9GC4tN.js.map → backup-service-EwcDVG-8.js.map} +1 -1
  11. package/dist/{cli-errors-B8qHg02P.js → cli-errors-DsA-K9uP.js} +96 -1
  12. package/dist/{cli-errors-B8qHg02P.js.map → cli-errors-DsA-K9uP.js.map} +1 -1
  13. package/dist/cli-errors.d.ts +42 -0
  14. package/dist/cli-helpers.d.ts +12 -12
  15. package/dist/cli.js +91 -37
  16. package/dist/cli.js.map +1 -1
  17. package/dist/{column-plan-helpers-DF-8dTVa.js → column-plan-helpers-1-LQD0yI.js} +34 -24
  18. package/dist/column-plan-helpers-1-LQD0yI.js.map +1 -0
  19. package/dist/data-transformer.d.ts +0 -8
  20. package/dist/{doctor-Cb2thZ8s.js → doctor-C-sYWbmt.js} +224 -43
  21. package/dist/doctor-C-sYWbmt.js.map +1 -0
  22. package/dist/{ensure-collection-policies-RHUcEp4v.js → ensure-collection-policies-B1ureSIV.js} +73 -18
  23. package/dist/ensure-collection-policies-B1ureSIV.js.map +1 -0
  24. package/dist/{ensure-collection-tables-BEEjn5cn.js → ensure-collection-tables-kkkHk8oo.js} +76 -20
  25. package/dist/ensure-collection-tables-kkkHk8oo.js.map +1 -0
  26. package/dist/{ensure-tables-Dhn9KM3B.js → ensure-tables-BmI_tRxc.js} +10 -3
  27. package/dist/ensure-tables-BmI_tRxc.js.map +1 -0
  28. package/dist/{generate-drizzle-schema-yzY_BLhr.js → generate-drizzle-schema-93M0lUxK.js} +2 -2
  29. package/dist/{generate-drizzle-schema-yzY_BLhr.js.map → generate-drizzle-schema-93M0lUxK.js.map} +1 -1
  30. package/dist/{generate-drizzle-schema-logic-BfzK7UQd.js → generate-drizzle-schema-logic-sSFDp6LR.js} +26 -8
  31. package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +1 -0
  32. package/dist/generate-postgres-ddl-logic-BJsLaVNX.js +152 -0
  33. package/dist/generate-postgres-ddl-logic-BJsLaVNX.js.map +1 -0
  34. package/dist/generated-sql.d.ts +28 -0
  35. package/dist/index.es.js +3415 -917
  36. package/dist/index.es.js.map +1 -1
  37. package/dist/{introspect-db-logic-WMuAfxvw.js → introspect-db-logic-kCETE8TY.js} +523 -349
  38. package/dist/introspect-db-logic-kCETE8TY.js.map +1 -0
  39. package/dist/introspect-db-queries-C_Q5VgQw.js +317 -0
  40. package/dist/introspect-db-queries-C_Q5VgQw.js.map +1 -0
  41. package/dist/{plan-schema-C0fxM8dY.js → plan-schema-DU9exq6C.js} +139 -504
  42. package/dist/plan-schema-DU9exq6C.js.map +1 -0
  43. package/dist/{policy-drift-DljYdrpW.js → policy-drift-B-J2hhm0.js} +3 -3
  44. package/dist/policy-drift-B-J2hhm0.js.map +1 -0
  45. package/dist/{generate-postgres-ddl-logic-Bt2d2mRH.js → render-ddl-Ds2t_d9V.js} +11 -149
  46. package/dist/render-ddl-Ds2t_d9V.js.map +1 -0
  47. package/dist/{rls-bootstrap-sql-B8EclDyM.js → rls-bootstrap-sql-_KNnjanK.js} +263 -39
  48. package/dist/rls-bootstrap-sql-_KNnjanK.js.map +1 -0
  49. package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-CfXOJJaW.js} +10 -2
  50. package/dist/rls-enforcement-CfXOJJaW.js.map +1 -0
  51. package/dist/schema/auth-schema.d.ts +170 -0
  52. package/dist/schema/classify-change.d.ts +29 -1
  53. package/dist/schema/column-plan-helpers.d.ts +33 -21
  54. package/dist/schema/destructive-sql.d.ts +20 -1
  55. package/dist/schema/doctor-cli.js +4 -4
  56. package/dist/schema/doctor.d.ts +34 -1
  57. package/dist/schema/ensure-collection-policies.d.ts +22 -0
  58. package/dist/schema/generate-drizzle-schema.js +1 -1
  59. package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
  60. package/dist/schema/generate-postgres-ddl.js +1 -1
  61. package/dist/schema/generate-schema-commit.d.ts +12 -0
  62. package/dist/schema/introspect-db-logic.d.ts +31 -0
  63. package/dist/schema/introspect-db-queries.d.ts +1 -1
  64. package/dist/schema/introspect-db-search.d.ts +22 -0
  65. package/dist/schema/introspect-db-storage.d.ts +83 -0
  66. package/dist/schema/introspect-db.js +15 -3
  67. package/dist/schema/introspect-db.js.map +1 -1
  68. package/dist/schema/plan/diff-plan.d.ts +1 -1
  69. package/dist/schema/plan/plan-schema.d.ts +16 -8
  70. package/dist/schema/plan/render-ddl.d.ts +6 -0
  71. package/dist/schema/plan/types.d.ts +37 -11
  72. package/dist/search-column-BM-GV6vH.js +442 -0
  73. package/dist/search-column-BM-GV6vH.js.map +1 -0
  74. package/dist/security/policy-drift.d.ts +1 -1
  75. package/dist/security/rls-enforcement.d.ts +7 -0
  76. package/dist/services/BranchService.d.ts +22 -1
  77. package/dist/services/FetchService.d.ts +5 -4
  78. package/dist/services/RelationService.d.ts +18 -0
  79. package/dist/services/cdc/CdcListener.d.ts +9 -4
  80. package/dist/services/cdc/identity-columns.d.ts +19 -0
  81. package/dist/services/cdc/trigger-cdc.d.ts +45 -7
  82. package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
  83. package/dist/services/channel-history.d.ts +17 -1
  84. package/dist/services/collection-helpers.d.ts +11 -1
  85. package/dist/services/pg-notify-listener.d.ts +88 -6
  86. package/dist/services/realtimeService.d.ts +159 -45
  87. package/dist/services/socket-liveness.d.ts +45 -0
  88. package/dist/services/soft-delete.d.ts +12 -0
  89. package/dist/services/sql-script.d.ts +71 -0
  90. package/dist/services/write-depth.d.ts +16 -0
  91. package/dist/services/write-transaction-scope.d.ts +6 -3
  92. package/dist/utils/drizzle-conditions.d.ts +42 -2
  93. package/dist/utils/sql-redaction.d.ts +21 -0
  94. package/dist/websocket.d.ts +50 -18
  95. package/package.json +7 -7
  96. package/dist/BranchService-DGPL6G_C.js.map +0 -1
  97. package/dist/column-plan-helpers-DF-8dTVa.js.map +0 -1
  98. package/dist/doctor-Cb2thZ8s.js.map +0 -1
  99. package/dist/ensure-collection-policies-RHUcEp4v.js.map +0 -1
  100. package/dist/ensure-collection-tables-BEEjn5cn.js.map +0 -1
  101. package/dist/ensure-tables-Dhn9KM3B.js.map +0 -1
  102. package/dist/generate-drizzle-schema-logic-BfzK7UQd.js.map +0 -1
  103. package/dist/generate-postgres-ddl-logic-Bt2d2mRH.js.map +0 -1
  104. package/dist/introspect-db-logic-WMuAfxvw.js.map +0 -1
  105. package/dist/plan-schema-C0fxM8dY.js.map +0 -1
  106. package/dist/policy-drift-DljYdrpW.js.map +0 -1
  107. package/dist/rls-bootstrap-sql-B8EclDyM.js.map +0 -1
  108. package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
@@ -0,0 +1,317 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ __createRequire(import.meta.url);
3
+ //#region src/schema/introspect-db-queries.ts
4
+ /**
5
+ * A Postgres identifier, quoted so that whatever is inside it stays inside it.
6
+ *
7
+ * Introspection reads table and schema names out of `pg_class` and puts them
8
+ * straight back into SQL, and wrapping a name in double quotes is not the same
9
+ * as escaping it: a table named `x" ; DROP TABLE users; --` closes the quote
10
+ * and then the statement. Postgres's own escape for a quote inside a quoted
11
+ * identifier is to double it, which is what this does.
12
+ *
13
+ * "The catalogue is trusted" is true of your own database, and is exactly the
14
+ * assumption this tool cannot make. Introspection exists to be pointed at
15
+ * databases somebody else built, and it runs with the privileges of whoever
16
+ * ran it — which is usually a superuser, because that is who has a connection
17
+ * string to a database they are adopting.
18
+ */
19
+ function quoteIdentifier(name) {
20
+ return `"${name.replace(/"/g, "\"\"")}"`;
21
+ }
22
+ /** `"schema"."table"`, both halves quoted. */
23
+ function quoteRelation(schema, table) {
24
+ return `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
25
+ }
26
+ /**
27
+ * Can this table's name be the name of the file describing it?
28
+ *
29
+ * Introspection writes `<collectionsDir>/<tableName>.ts`, and the table name
30
+ * came from a database this tool did not create. Postgres will hold a table
31
+ * called `../../../etc/cron.d/x`, and `path.join` resolves it — so without this
32
+ * the generator writes a file wherever the name points, as whoever ran the
33
+ * command.
34
+ *
35
+ * Deliberately narrow, and deliberately not a blocklist of `..` and `/`: the
36
+ * question is "is this a plain file name?", and every answer that is not
37
+ * obviously yes is a table better written by hand. NUL and the Windows
38
+ * separator are in scope because a collections directory is checked out on
39
+ * whatever machine the developer has.
40
+ */
41
+ function isUsableFileName(name) {
42
+ if (name.length === 0 || name.length > 200) return false;
43
+ if (name === "." || name === "..") return false;
44
+ if (/[/\\:\u0000-\u001f]/.test(name)) return false;
45
+ return !name.startsWith(".") && !name.endsWith(".");
46
+ }
47
+ /**
48
+ * Base tables, excluding partitions.
49
+ *
50
+ * `relispartition` is the reason this reads `pg_class` rather than
51
+ * `information_schema.tables`, which reports every partition as a base table of
52
+ * its own. Pagila partitions `payment` by month; introspected through
53
+ * information_schema it yields `payment` plus 26 near-identical
54
+ * `payment_p2022_*` collections, each with its own nav entry.
55
+ *
56
+ * `relkind = 'p'` (the partitioned parent) is included and `'r'` partitions are
57
+ * dropped, which is the right way round: the parent is the queryable table.
58
+ */
59
+ var TABLES_QUERY = `
60
+ SELECT c.relname AS table_name,
61
+ c.relkind = 'p' AS is_partitioned
62
+ FROM pg_class c
63
+ JOIN pg_namespace n ON n.oid = c.relnamespace
64
+ WHERE n.nspname = $1
65
+ AND c.relkind IN ('r', 'p')
66
+ AND NOT c.relispartition
67
+ AND c.relname NOT LIKE 'drizzle_%'
68
+ AND c.relname NOT LIKE 'rebase_%'
69
+ ORDER BY c.relname
70
+ `;
71
+ /**
72
+ * Columns, with the catalog facts that say a column is not the user's to edit.
73
+ *
74
+ * `is_generated`/`is_identity` are read for that reason: a generated column
75
+ * rejects any write, so a form that offers it is offering a field that always
76
+ * errors. `character_maximum_length` is the declared `varchar(n)` bound —
77
+ * already enforced by the database, and free to surface as validation.
78
+ */
79
+ var COLUMNS_QUERY = `
80
+ SELECT
81
+ c.table_name,
82
+ c.column_name,
83
+ c.data_type,
84
+ c.udt_name,
85
+ c.is_nullable,
86
+ c.column_default,
87
+ c.ordinal_position,
88
+ c.is_generated,
89
+ c.generation_expression,
90
+ c.is_identity,
91
+ c.identity_generation,
92
+ c.character_maximum_length,
93
+ c.numeric_precision,
94
+ c.numeric_scale,
95
+ (SELECT a.atttypmod FROM pg_attribute a
96
+ JOIN pg_class pc ON a.attrelid = pc.oid
97
+ WHERE pc.relname = c.table_name
98
+ AND a.attname = c.column_name
99
+ AND pc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema)) as atttypmod
100
+ FROM information_schema.columns c
101
+ WHERE c.table_schema = $1
102
+ ORDER BY c.table_name, c.ordinal_position
103
+ `;
104
+ var ENUMS_QUERY = `
105
+ SELECT t.typname AS enum_name,
106
+ e.enumlabel AS enum_value,
107
+ e.enumsortorder AS sort_order
108
+ FROM pg_type t
109
+ JOIN pg_enum e ON t.oid = e.enumtypid
110
+ JOIN pg_namespace n ON t.typnamespace = n.oid
111
+ WHERE n.nspname = $1
112
+ ORDER BY t.typname, e.enumsortorder
113
+ `;
114
+ /**
115
+ * Primary key columns, in key order.
116
+ *
117
+ * `ORDER BY k.ord` matters for composite keys: the position of a column inside
118
+ * the key is what tells a two-column PK made of two foreign keys apart from an
119
+ * ordinary composite key, and `= ANY(i.indkey)` (the shape this replaced)
120
+ * returns catalog order instead.
121
+ */
122
+ var PRIMARY_KEYS_QUERY = `
123
+ SELECT t.relname AS table_name,
124
+ a.attname AS column_name
125
+ FROM pg_index i
126
+ JOIN pg_class t ON t.oid = i.indrelid
127
+ JOIN pg_namespace n ON n.oid = t.relnamespace
128
+ CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS k(attnum, ord)
129
+ JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum
130
+ WHERE i.indisprimary AND n.nspname = $1
131
+ ORDER BY t.relname, k.ord
132
+ `;
133
+ /**
134
+ * Foreign keys, one row per referencing column, with the delete rule.
135
+ *
136
+ * Read from `pg_constraint` rather than through
137
+ * `information_schema.constraint_column_usage`, which does not preserve the
138
+ * pairing on a composite foreign key: it reports the cross product of the
139
+ * constraint's columns, so a two-column FK comes back as four rows, two of them
140
+ * pairing the wrong columns. `unnest(...) WITH ORDINALITY` on both sides joined
141
+ * on the ordinal keeps each referencing column with the column it references.
142
+ *
143
+ * `confdeltype` is carried because `ON DELETE CASCADE` is the schema author
144
+ * saying, in the database, that the child cannot outlive the parent — the one
145
+ * unambiguous declaration of ownership a schema contains.
146
+ *
147
+ * Constraints declared on a *partition* are attributed to the partition root,
148
+ * because that is the table this run generates a collection for. Pagila declares
149
+ * `payment`'s three foreign keys on each monthly partition and none on the
150
+ * parent, so reading `pg_constraint` at face value gives the `payment`
151
+ * collection no relations at all. `pick = 1` then keeps one row per logical key,
152
+ * preferring the root's own constraint when it has one, so a schema that
153
+ * declares the key on the parent *and* inherits it onto 26 partitions does not
154
+ * yield 27 copies of the same relation.
155
+ */
156
+ var FOREIGN_KEYS_QUERY = `
157
+ WITH fk AS (
158
+ SELECT con.oid,
159
+ con.conname,
160
+ con.conrelid,
161
+ con.confrelid,
162
+ con.conkey,
163
+ con.confkey,
164
+ con.confdeltype,
165
+ COALESCE(pg_partition_root(con.conrelid), con.conrelid) AS root_oid,
166
+ row_number() OVER (
167
+ PARTITION BY COALESCE(pg_partition_root(con.conrelid), con.conrelid),
168
+ con.confrelid, con.conkey, con.confkey
169
+ ORDER BY (con.conrelid = COALESCE(pg_partition_root(con.conrelid), con.conrelid)) DESC,
170
+ con.oid
171
+ ) AS pick
172
+ FROM pg_constraint con
173
+ JOIN pg_namespace n ON n.oid = con.connamespace
174
+ WHERE con.contype = 'f' AND n.nspname = $1
175
+ )
176
+ SELECT root.relname AS table_name,
177
+ sa.attname AS column_name,
178
+ tgt.relname AS foreign_table_name,
179
+ ta.attname AS foreign_column_name,
180
+ fk.conname AS constraint_name,
181
+ s.ord::int AS ordinal,
182
+ CASE fk.confdeltype
183
+ WHEN 'a' THEN 'NO ACTION'
184
+ WHEN 'r' THEN 'RESTRICT'
185
+ WHEN 'c' THEN 'CASCADE'
186
+ WHEN 'n' THEN 'SET NULL'
187
+ WHEN 'd' THEN 'SET DEFAULT'
188
+ END AS delete_rule
189
+ FROM fk
190
+ JOIN pg_class root ON root.oid = fk.root_oid
191
+ JOIN pg_class tgt ON tgt.oid = fk.confrelid
192
+ CROSS JOIN LATERAL unnest(fk.conkey) WITH ORDINALITY AS s(attnum, ord)
193
+ JOIN pg_attribute sa ON sa.attrelid = fk.conrelid AND sa.attnum = s.attnum
194
+ CROSS JOIN LATERAL unnest(fk.confkey) WITH ORDINALITY AS f(attnum, ord)
195
+ JOIN pg_attribute ta ON ta.attrelid = fk.confrelid AND ta.attnum = f.attnum
196
+ WHERE fk.pick = 1 AND s.ord = f.ord
197
+ ORDER BY root.relname, fk.conname, s.ord
198
+ `;
199
+ /**
200
+ * Unique constraints and unique indexes, as ordered column lists.
201
+ *
202
+ * Indexes rather than constraints alone, because `CREATE UNIQUE INDEX` and
203
+ * `ADD CONSTRAINT ... UNIQUE` produce the same guarantee and schemas use both.
204
+ * Partial indexes (`indpred IS NOT NULL`) are excluded: they promise uniqueness
205
+ * only over the rows matching their predicate, which is not the promise a
206
+ * `validation: { unique: true }` field makes.
207
+ */
208
+ var UNIQUE_CONSTRAINTS_QUERY = `
209
+ SELECT t.relname AS table_name,
210
+ ic.relname AS constraint_name,
211
+ -- ::text, because the driver has no parser for an array of \`name\`
212
+ -- and hands back the raw \`{a,b}\` literal as a string. Every consumer
213
+ -- here indexes and compares it as an array, and a string of that
214
+ -- shape fails those silently rather than loudly.
215
+ array_agg(a.attname::text ORDER BY k.ord) AS column_names
216
+ FROM pg_index i
217
+ JOIN pg_class t ON t.oid = i.indrelid
218
+ JOIN pg_class ic ON ic.oid = i.indexrelid
219
+ JOIN pg_namespace n ON n.oid = t.relnamespace
220
+ CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS k(attnum, ord)
221
+ JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum
222
+ WHERE i.indisunique
223
+ AND NOT i.indisprimary
224
+ AND i.indpred IS NULL
225
+ AND n.nspname = $1
226
+ AND NOT t.relispartition
227
+ GROUP BY t.relname, ic.relname
228
+ ORDER BY t.relname, ic.relname
229
+ `;
230
+ /**
231
+ * CHECK constraints, as the source text Postgres reproduces them from.
232
+ *
233
+ * `pg_get_constraintdef` output is normalized by the server — the parser in
234
+ * {@link ./introspect-db-constraints.ts} reads that normalized form, not
235
+ * whatever the author typed.
236
+ */
237
+ var CHECK_CONSTRAINTS_QUERY = `
238
+ SELECT DISTINCT
239
+ root.relname AS table_name,
240
+ con.conname AS constraint_name,
241
+ pg_get_constraintdef(con.oid) AS definition
242
+ FROM pg_constraint con
243
+ JOIN pg_namespace n ON n.oid = con.connamespace
244
+ JOIN pg_class root ON root.oid = COALESCE(pg_partition_root(con.conrelid), con.conrelid)
245
+ WHERE con.contype = 'c'
246
+ AND n.nspname = $1
247
+ ORDER BY root.relname, con.conname
248
+ `;
249
+ /**
250
+ * `COMMENT ON TABLE` / `COMMENT ON COLUMN`, which is documentation the author
251
+ * already wrote and which nothing downstream has ever read.
252
+ */
253
+ var COMMENTS_QUERY = `
254
+ SELECT t.relname AS table_name,
255
+ a.attname AS column_name,
256
+ d.description AS comment
257
+ FROM pg_description d
258
+ JOIN pg_class t ON t.oid = d.objoid
259
+ JOIN pg_namespace n ON n.oid = t.relnamespace
260
+ LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.objsubid
261
+ WHERE n.nspname = $1
262
+ AND t.relkind IN ('r', 'p')
263
+ AND (d.objsubid = 0 OR a.attname IS NOT NULL)
264
+ ORDER BY t.relname, d.objsubid
265
+ `;
266
+ /**
267
+ * Counts a table's rows, stopping once the answer can only be "more than
268
+ * `limit`".
269
+ *
270
+ * Introspection asks this to tell a small reference table from a table that
271
+ * merely looks like one (see `classifyTables`). `reltuples` would answer for
272
+ * free but lies on a table that has never been analyzed — which is every table
273
+ * in a database restored from a dump, i.e. exactly the case introspection meets
274
+ * — and reports -1 there rather than an error, so the lie is silent.
275
+ *
276
+ * The subquery's LIMIT caps the work at `limit + 1` rows however large the
277
+ * table is, so this stays cheap on a table with a billion rows.
278
+ */
279
+ async function countRowsUpTo(client, schema, table, limit) {
280
+ const { rows } = await client.query(`SELECT count(*)::text AS count FROM (SELECT 1 FROM ${quoteRelation(schema, table)} LIMIT ${limit + 1}) probe`);
281
+ return Number(rows[0]?.count ?? 0);
282
+ }
283
+ /**
284
+ * Reads everything the generator needs, in one pass.
285
+ *
286
+ * `rowCounts` is deliberately absent here: it costs a query per table and only
287
+ * a handful of tables can possibly need it. The caller fills it in for the
288
+ * candidates `lookupCandidates()` names.
289
+ */
290
+ async function readSchemaMetadata(client, schema) {
291
+ const tables = await client.query(TABLES_QUERY, [schema]);
292
+ const columns = await client.query(COLUMNS_QUERY, [schema]);
293
+ const enumValues = await client.query(ENUMS_QUERY, [schema]);
294
+ const pks = await client.query(PRIMARY_KEYS_QUERY, [schema]);
295
+ const fks = await client.query(FOREIGN_KEYS_QUERY, [schema]);
296
+ const uniques = await client.query(UNIQUE_CONSTRAINTS_QUERY, [schema]);
297
+ const checks = await client.query(CHECK_CONSTRAINTS_QUERY, [schema]);
298
+ const comments = await client.query(COMMENTS_QUERY, [schema]);
299
+ const known = new Set(tables.rows.map((t) => t.table_name));
300
+ const scoped = (rows) => rows.filter((r) => known.has(r.table_name));
301
+ return {
302
+ schema,
303
+ tables: tables.rows,
304
+ columns: scoped(columns.rows),
305
+ enumValues: enumValues.rows,
306
+ pks: scoped(pks.rows),
307
+ fks: scoped(fks.rows).filter((fk) => known.has(fk.foreign_table_name)),
308
+ uniques: scoped(uniques.rows),
309
+ checks: scoped(checks.rows),
310
+ comments: scoped(comments.rows),
311
+ rowCounts: {}
312
+ };
313
+ }
314
+ //#endregion
315
+ export { readSchemaMetadata as i, isUsableFileName as n, quoteRelation as r, countRowsUpTo as t };
316
+
317
+ //# sourceMappingURL=introspect-db-queries-C_Q5VgQw.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"introspect-db-queries-C_Q5VgQw.js","names":[],"sources":["../src/schema/introspect-db-queries.ts"],"sourcesContent":["/**\n * The catalog queries introspection runs, and the shape they produce.\n *\n * These live apart from {@link ./introspect-db.ts} (the CLI entry point) on\n * purpose: the fixture-capture script and the live e2e read a database through\n * this module too, so what the tests see is what the CLI sees. A query that\n * only exists inside the CLI's `main()` can only be tested by running the CLI.\n *\n * No side effects beyond `SELECT`. Introspection reads a database it does not\n * own — it must never `ANALYZE`, create a temp table, or otherwise write.\n */\nimport type {\n TableRow,\n TableColumn,\n EnumValue,\n PrimaryKeyRow,\n ForeignKeyRow,\n UniqueConstraintRow,\n CheckConstraintRow,\n CommentRow,\n SchemaMetadata\n} from \"./introspect-db-logic\";\n\n/** The subset of `pg.Client` this module needs, so tests can pass a fake. */\nexport interface QueryableClient {\n query<R = Record<string, unknown>>(text: string, values?: unknown[]): Promise<{ rows: R[] }>;\n}\n\n/**\n * A Postgres identifier, quoted so that whatever is inside it stays inside it.\n *\n * Introspection reads table and schema names out of `pg_class` and puts them\n * straight back into SQL, and wrapping a name in double quotes is not the same\n * as escaping it: a table named `x\" ; DROP TABLE users; --` closes the quote\n * and then the statement. Postgres's own escape for a quote inside a quoted\n * identifier is to double it, which is what this does.\n *\n * \"The catalogue is trusted\" is true of your own database, and is exactly the\n * assumption this tool cannot make. Introspection exists to be pointed at\n * databases somebody else built, and it runs with the privileges of whoever\n * ran it — which is usually a superuser, because that is who has a connection\n * string to a database they are adopting.\n */\nexport function quoteIdentifier(name: string): string {\n return `\"${name.replace(/\"/g, \"\\\"\\\"\")}\"`;\n}\n\n/** `\"schema\".\"table\"`, both halves quoted. */\nexport function quoteRelation(schema: string, table: string): string {\n return `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;\n}\n\n/**\n * Can this table's name be the name of the file describing it?\n *\n * Introspection writes `<collectionsDir>/<tableName>.ts`, and the table name\n * came from a database this tool did not create. Postgres will hold a table\n * called `../../../etc/cron.d/x`, and `path.join` resolves it — so without this\n * the generator writes a file wherever the name points, as whoever ran the\n * command.\n *\n * Deliberately narrow, and deliberately not a blocklist of `..` and `/`: the\n * question is \"is this a plain file name?\", and every answer that is not\n * obviously yes is a table better written by hand. NUL and the Windows\n * separator are in scope because a collections directory is checked out on\n * whatever machine the developer has.\n */\nexport function isUsableFileName(name: string): boolean {\n if (name.length === 0 || name.length > 200) return false;\n if (name === \".\" || name === \"..\") return false;\n // Path separators (both platforms'), the Windows drive separator, and any\n // control character — a NUL truncates the path at the OS boundary, so a\n // name that reads as harmless in a log can be something else on disk.\n // eslint-disable-next-line no-control-regex\n if (/[/\\\\:\\u0000-\\u001f]/.test(name)) return false;\n // A leading dot makes a hidden file, and a trailing one is invalid on\n // Windows; neither is a table anyone means to generate.\n return !name.startsWith(\".\") && !name.endsWith(\".\");\n}\n\n/**\n * Base tables, excluding partitions.\n *\n * `relispartition` is the reason this reads `pg_class` rather than\n * `information_schema.tables`, which reports every partition as a base table of\n * its own. Pagila partitions `payment` by month; introspected through\n * information_schema it yields `payment` plus 26 near-identical\n * `payment_p2022_*` collections, each with its own nav entry.\n *\n * `relkind = 'p'` (the partitioned parent) is included and `'r'` partitions are\n * dropped, which is the right way round: the parent is the queryable table.\n */\nexport const TABLES_QUERY = `\n SELECT c.relname AS table_name,\n c.relkind = 'p' AS is_partitioned\n FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE n.nspname = $1\n AND c.relkind IN ('r', 'p')\n AND NOT c.relispartition\n AND c.relname NOT LIKE 'drizzle_%'\n AND c.relname NOT LIKE 'rebase_%'\n ORDER BY c.relname\n`;\n\n/**\n * Columns, with the catalog facts that say a column is not the user's to edit.\n *\n * `is_generated`/`is_identity` are read for that reason: a generated column\n * rejects any write, so a form that offers it is offering a field that always\n * errors. `character_maximum_length` is the declared `varchar(n)` bound —\n * already enforced by the database, and free to surface as validation.\n */\nexport const COLUMNS_QUERY = `\n SELECT\n c.table_name,\n c.column_name,\n c.data_type,\n c.udt_name,\n c.is_nullable,\n c.column_default,\n c.ordinal_position,\n c.is_generated,\n c.generation_expression,\n c.is_identity,\n c.identity_generation,\n c.character_maximum_length,\n c.numeric_precision,\n c.numeric_scale,\n (SELECT a.atttypmod FROM pg_attribute a\n JOIN pg_class pc ON a.attrelid = pc.oid\n WHERE pc.relname = c.table_name\n AND a.attname = c.column_name\n AND pc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema)) as atttypmod\n FROM information_schema.columns c\n WHERE c.table_schema = $1\n ORDER BY c.table_name, c.ordinal_position\n`;\n\nexport const ENUMS_QUERY = `\n SELECT t.typname AS enum_name,\n e.enumlabel AS enum_value,\n e.enumsortorder AS sort_order\n FROM pg_type t\n JOIN pg_enum e ON t.oid = e.enumtypid\n JOIN pg_namespace n ON t.typnamespace = n.oid\n WHERE n.nspname = $1\n ORDER BY t.typname, e.enumsortorder\n`;\n\n/**\n * Primary key columns, in key order.\n *\n * `ORDER BY k.ord` matters for composite keys: the position of a column inside\n * the key is what tells a two-column PK made of two foreign keys apart from an\n * ordinary composite key, and `= ANY(i.indkey)` (the shape this replaced)\n * returns catalog order instead.\n */\nexport const PRIMARY_KEYS_QUERY = `\n SELECT t.relname AS table_name,\n a.attname AS column_name\n FROM pg_index i\n JOIN pg_class t ON t.oid = i.indrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS k(attnum, ord)\n JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum\n WHERE i.indisprimary AND n.nspname = $1\n ORDER BY t.relname, k.ord\n`;\n\n/**\n * Foreign keys, one row per referencing column, with the delete rule.\n *\n * Read from `pg_constraint` rather than through\n * `information_schema.constraint_column_usage`, which does not preserve the\n * pairing on a composite foreign key: it reports the cross product of the\n * constraint's columns, so a two-column FK comes back as four rows, two of them\n * pairing the wrong columns. `unnest(...) WITH ORDINALITY` on both sides joined\n * on the ordinal keeps each referencing column with the column it references.\n *\n * `confdeltype` is carried because `ON DELETE CASCADE` is the schema author\n * saying, in the database, that the child cannot outlive the parent — the one\n * unambiguous declaration of ownership a schema contains.\n *\n * Constraints declared on a *partition* are attributed to the partition root,\n * because that is the table this run generates a collection for. Pagila declares\n * `payment`'s three foreign keys on each monthly partition and none on the\n * parent, so reading `pg_constraint` at face value gives the `payment`\n * collection no relations at all. `pick = 1` then keeps one row per logical key,\n * preferring the root's own constraint when it has one, so a schema that\n * declares the key on the parent *and* inherits it onto 26 partitions does not\n * yield 27 copies of the same relation.\n */\nexport const FOREIGN_KEYS_QUERY = `\n WITH fk AS (\n SELECT con.oid,\n con.conname,\n con.conrelid,\n con.confrelid,\n con.conkey,\n con.confkey,\n con.confdeltype,\n COALESCE(pg_partition_root(con.conrelid), con.conrelid) AS root_oid,\n row_number() OVER (\n PARTITION BY COALESCE(pg_partition_root(con.conrelid), con.conrelid),\n con.confrelid, con.conkey, con.confkey\n ORDER BY (con.conrelid = COALESCE(pg_partition_root(con.conrelid), con.conrelid)) DESC,\n con.oid\n ) AS pick\n FROM pg_constraint con\n JOIN pg_namespace n ON n.oid = con.connamespace\n WHERE con.contype = 'f' AND n.nspname = $1\n )\n SELECT root.relname AS table_name,\n sa.attname AS column_name,\n tgt.relname AS foreign_table_name,\n ta.attname AS foreign_column_name,\n fk.conname AS constraint_name,\n s.ord::int AS ordinal,\n CASE fk.confdeltype\n WHEN 'a' THEN 'NO ACTION'\n WHEN 'r' THEN 'RESTRICT'\n WHEN 'c' THEN 'CASCADE'\n WHEN 'n' THEN 'SET NULL'\n WHEN 'd' THEN 'SET DEFAULT'\n END AS delete_rule\n FROM fk\n JOIN pg_class root ON root.oid = fk.root_oid\n JOIN pg_class tgt ON tgt.oid = fk.confrelid\n CROSS JOIN LATERAL unnest(fk.conkey) WITH ORDINALITY AS s(attnum, ord)\n JOIN pg_attribute sa ON sa.attrelid = fk.conrelid AND sa.attnum = s.attnum\n CROSS JOIN LATERAL unnest(fk.confkey) WITH ORDINALITY AS f(attnum, ord)\n JOIN pg_attribute ta ON ta.attrelid = fk.confrelid AND ta.attnum = f.attnum\n WHERE fk.pick = 1 AND s.ord = f.ord\n ORDER BY root.relname, fk.conname, s.ord\n`;\n\n/**\n * Unique constraints and unique indexes, as ordered column lists.\n *\n * Indexes rather than constraints alone, because `CREATE UNIQUE INDEX` and\n * `ADD CONSTRAINT ... UNIQUE` produce the same guarantee and schemas use both.\n * Partial indexes (`indpred IS NOT NULL`) are excluded: they promise uniqueness\n * only over the rows matching their predicate, which is not the promise a\n * `validation: { unique: true }` field makes.\n */\nexport const UNIQUE_CONSTRAINTS_QUERY = `\n SELECT t.relname AS table_name,\n ic.relname AS constraint_name,\n -- ::text, because the driver has no parser for an array of \\`name\\`\n -- and hands back the raw \\`{a,b}\\` literal as a string. Every consumer\n -- here indexes and compares it as an array, and a string of that\n -- shape fails those silently rather than loudly.\n array_agg(a.attname::text ORDER BY k.ord) AS column_names\n FROM pg_index i\n JOIN pg_class t ON t.oid = i.indrelid\n JOIN pg_class ic ON ic.oid = i.indexrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS k(attnum, ord)\n JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum\n WHERE i.indisunique\n AND NOT i.indisprimary\n AND i.indpred IS NULL\n AND n.nspname = $1\n AND NOT t.relispartition\n GROUP BY t.relname, ic.relname\n ORDER BY t.relname, ic.relname\n`;\n\n/**\n * CHECK constraints, as the source text Postgres reproduces them from.\n *\n * `pg_get_constraintdef` output is normalized by the server — the parser in\n * {@link ./introspect-db-constraints.ts} reads that normalized form, not\n * whatever the author typed.\n */\nexport const CHECK_CONSTRAINTS_QUERY = `\n SELECT DISTINCT\n root.relname AS table_name,\n con.conname AS constraint_name,\n pg_get_constraintdef(con.oid) AS definition\n FROM pg_constraint con\n JOIN pg_namespace n ON n.oid = con.connamespace\n JOIN pg_class root ON root.oid = COALESCE(pg_partition_root(con.conrelid), con.conrelid)\n WHERE con.contype = 'c'\n AND n.nspname = $1\n ORDER BY root.relname, con.conname\n`;\n\n/**\n * `COMMENT ON TABLE` / `COMMENT ON COLUMN`, which is documentation the author\n * already wrote and which nothing downstream has ever read.\n */\nexport const COMMENTS_QUERY = `\n SELECT t.relname AS table_name,\n a.attname AS column_name,\n d.description AS comment\n FROM pg_description d\n JOIN pg_class t ON t.oid = d.objoid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.objsubid\n WHERE n.nspname = $1\n AND t.relkind IN ('r', 'p')\n AND (d.objsubid = 0 OR a.attname IS NOT NULL)\n ORDER BY t.relname, d.objsubid\n`;\n\n/**\n * Counts a table's rows, stopping once the answer can only be \"more than\n * `limit`\".\n *\n * Introspection asks this to tell a small reference table from a table that\n * merely looks like one (see `classifyTables`). `reltuples` would answer for\n * free but lies on a table that has never been analyzed — which is every table\n * in a database restored from a dump, i.e. exactly the case introspection meets\n * — and reports -1 there rather than an error, so the lie is silent.\n *\n * The subquery's LIMIT caps the work at `limit + 1` rows however large the\n * table is, so this stays cheap on a table with a billion rows.\n */\nexport async function countRowsUpTo(\n client: QueryableClient,\n schema: string,\n table: string,\n limit: number\n): Promise<number> {\n const { rows } = await client.query<{ count: string }>(\n `SELECT count(*)::text AS count FROM (SELECT 1 FROM ${quoteRelation(schema, table)} LIMIT ${limit + 1}) probe`\n );\n return Number(rows[0]?.count ?? 0);\n}\n\n/**\n * Reads everything the generator needs, in one pass.\n *\n * `rowCounts` is deliberately absent here: it costs a query per table and only\n * a handful of tables can possibly need it. The caller fills it in for the\n * candidates `lookupCandidates()` names.\n */\nexport async function readSchemaMetadata(\n client: QueryableClient,\n schema: string\n): Promise<SchemaMetadata> {\n // Sequential, not `Promise.all`. A `pg.Client` is a single connection with\n // one protocol stream: concurrent `query()` calls on one are queued and\n // deprecated (removed in pg@9), so the parallel version buys nothing and\n // warns while doing it.\n const tables = await client.query<TableRow>(TABLES_QUERY, [schema]);\n const columns = await client.query<TableColumn>(COLUMNS_QUERY, [schema]);\n const enumValues = await client.query<EnumValue>(ENUMS_QUERY, [schema]);\n const pks = await client.query<PrimaryKeyRow>(PRIMARY_KEYS_QUERY, [schema]);\n const fks = await client.query<ForeignKeyRow>(FOREIGN_KEYS_QUERY, [schema]);\n const uniques = await client.query<UniqueConstraintRow>(UNIQUE_CONSTRAINTS_QUERY, [schema]);\n const checks = await client.query<CheckConstraintRow>(CHECK_CONSTRAINTS_QUERY, [schema]);\n const comments = await client.query<CommentRow>(COMMENTS_QUERY, [schema]);\n\n // A column list scoped to the schema still names tables this run excludes —\n // views, partitions, the drizzle bookkeeping tables. Narrow every row set to\n // the tables actually being introspected so downstream code never has to\n // ask whether a table it found in `columns` really exists.\n const known = new Set(tables.rows.map((t) => t.table_name));\n const scoped = <T extends { table_name: string }>(rows: T[]) => rows.filter((r) => known.has(r.table_name));\n\n return {\n schema,\n tables: tables.rows,\n columns: scoped(columns.rows),\n enumValues: enumValues.rows,\n pks: scoped(pks.rows),\n // A foreign key pointing *out* of the introspected set (into another\n // schema, or at a view) cannot be turned into a relation: there is no\n // collection on the other end to link to.\n fks: scoped(fks.rows).filter((fk) => known.has(fk.foreign_table_name)),\n uniques: scoped(uniques.rows),\n checks: scoped(checks.rows),\n comments: scoped(comments.rows),\n rowCounts: {}\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA2CA,SAAgB,gBAAgB,MAAsB;CAClD,OAAO,IAAI,KAAK,QAAQ,MAAM,MAAM,EAAE;AAC1C;;AAGA,SAAgB,cAAc,QAAgB,OAAuB;CACjE,OAAO,GAAG,gBAAgB,MAAM,EAAE,GAAG,gBAAgB,KAAK;AAC9D;;;;;;;;;;;;;;;;AAiBA,SAAgB,iBAAiB,MAAuB;CACpD,IAAI,KAAK,WAAW,KAAK,KAAK,SAAS,KAAK,OAAO;CACnD,IAAI,SAAS,OAAO,SAAS,MAAM,OAAO;CAK1C,IAAI,sBAAsB,KAAK,IAAI,GAAG,OAAO;CAG7C,OAAO,CAAC,KAAK,WAAW,GAAG,KAAK,CAAC,KAAK,SAAS,GAAG;AACtD;;;;;;;;;;;;;AAcA,IAAa,eAAe;;;;;;;;;;;;;;;;;;;;AAqB5B,IAAa,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;AA0B7B,IAAa,cAAc;;;;;;;;;;;;;;;;;;AAmB3B,IAAa,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmClC,IAAa,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDlC,IAAa,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BxC,IAAa,0BAA0B;;;;;;;;;;;;;;;;AAiBvC,IAAa,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2B9B,eAAsB,cAClB,QACA,QACA,OACA,OACe;CACf,MAAM,EAAE,SAAS,MAAM,OAAO,MAC1B,sDAAsD,cAAc,QAAQ,KAAK,EAAE,SAAS,QAAQ,EAAE,QAC1G;CACA,OAAO,OAAO,KAAK,EAAE,EAAE,SAAS,CAAC;AACrC;;;;;;;;AASA,eAAsB,mBAClB,QACA,QACuB;CAKvB,MAAM,SAAS,MAAM,OAAO,MAAgB,cAAc,CAAC,MAAM,CAAC;CAClE,MAAM,UAAU,MAAM,OAAO,MAAmB,eAAe,CAAC,MAAM,CAAC;CACvE,MAAM,aAAa,MAAM,OAAO,MAAiB,aAAa,CAAC,MAAM,CAAC;CACtE,MAAM,MAAM,MAAM,OAAO,MAAqB,oBAAoB,CAAC,MAAM,CAAC;CAC1E,MAAM,MAAM,MAAM,OAAO,MAAqB,oBAAoB,CAAC,MAAM,CAAC;CAC1E,MAAM,UAAU,MAAM,OAAO,MAA2B,0BAA0B,CAAC,MAAM,CAAC;CAC1F,MAAM,SAAS,MAAM,OAAO,MAA0B,yBAAyB,CAAC,MAAM,CAAC;CACvF,MAAM,WAAW,MAAM,OAAO,MAAkB,gBAAgB,CAAC,MAAM,CAAC;CAMxE,MAAM,QAAQ,IAAI,IAAI,OAAO,KAAK,KAAK,MAAM,EAAE,UAAU,CAAC;CAC1D,MAAM,UAA4C,SAAc,KAAK,QAAQ,MAAM,MAAM,IAAI,EAAE,UAAU,CAAC;CAE1G,OAAO;EACH;EACA,QAAQ,OAAO;EACf,SAAS,OAAO,QAAQ,IAAI;EAC5B,YAAY,WAAW;EACvB,KAAK,OAAO,IAAI,IAAI;EAIpB,KAAK,OAAO,IAAI,IAAI,CAAC,CAAC,QAAQ,OAAO,MAAM,IAAI,GAAG,kBAAkB,CAAC;EACrE,SAAS,OAAO,QAAQ,IAAI;EAC5B,QAAQ,OAAO,OAAO,IAAI;EAC1B,UAAU,OAAO,SAAS,IAAI;EAC9B,WAAW,CAAC;CAChB;AACJ"}