@rebasepro/server-postgres 0.12.1-canary.gcfa333c → 0.12.1-canary.gddbd489

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 (81) hide show
  1. package/dist/PostgresBackendDriver.d.ts +1 -1
  2. package/dist/PostgresBootstrapper.d.ts +25 -1
  3. package/dist/auth/services.d.ts +6 -1
  4. package/dist/backup/backup-service.d.ts +10 -1
  5. package/dist/backup/pg-tools.d.ts +47 -0
  6. package/dist/{backup-service-DH9kPg-E.js → backup-service-CD8o_1Sl.js} +144 -11
  7. package/dist/backup-service-CD8o_1Sl.js.map +1 -0
  8. package/dist/cli-helpers.d.ts +39 -0
  9. package/dist/{connection-B5Wndbr1.js → connection-BuZ97wsr.js} +57 -3
  10. package/dist/connection-BuZ97wsr.js.map +1 -0
  11. package/dist/connection.d.ts +42 -0
  12. package/dist/{ensure-collection-policies-Vl3Cv1Q9.js → ensure-collection-policies-ViG8XiPn.js} +2 -2
  13. package/dist/{ensure-collection-policies-Vl3Cv1Q9.js.map → ensure-collection-policies-ViG8XiPn.js.map} +1 -1
  14. package/dist/{ensure-collection-tables-DsDsNl6o.js → ensure-collection-tables-CBQdOETu.js} +72 -12
  15. package/dist/ensure-collection-tables-CBQdOETu.js.map +1 -0
  16. package/dist/index.es.js +990 -677
  17. package/dist/index.es.js.map +1 -1
  18. package/dist/policy-CeA1JcxP.js +105 -0
  19. package/dist/policy-CeA1JcxP.js.map +1 -0
  20. package/dist/schema/ensure-collection-tables.d.ts +21 -1
  21. package/dist/schema/generate-postgres-ddl-logic.d.ts +19 -0
  22. package/dist/schema/introspect-db-constraints.d.ts +57 -0
  23. package/dist/schema/introspect-db-logic.d.ts +94 -5
  24. package/dist/schema/introspect-db-queries.d.ts +119 -0
  25. package/dist/schema/introspect-db-structure.d.ts +263 -0
  26. package/dist/schema/introspect-db-types.d.ts +11 -0
  27. package/dist/services/FetchService.d.ts +4 -1
  28. package/dist/services/RelationService.d.ts +24 -1
  29. package/dist/services/channel-bus/index.d.ts +1 -7
  30. package/dist/services/collection-helpers.d.ts +36 -2
  31. package/dist/services/dataService.d.ts +3 -1
  32. package/dist/services/row-pipeline.d.ts +1 -1
  33. package/dist/{src-DihrDFuP.js → src-DlPBctw_.js} +217 -101
  34. package/dist/src-DlPBctw_.js.map +1 -0
  35. package/dist/utils/connection-string.d.ts +29 -0
  36. package/dist/utils/drizzle-conditions.d.ts +5 -4
  37. package/dist/utils/pg-error-utils.d.ts +16 -0
  38. package/dist/{websocket-BKcGvILX.js → websocket-B2LsrINK.js} +8 -6
  39. package/dist/websocket-B2LsrINK.js.map +1 -0
  40. package/package.json +6 -6
  41. package/src/PostgresAdapter.ts +7 -2
  42. package/src/PostgresBackendDriver.ts +4 -0
  43. package/src/PostgresBootstrapper.ts +83 -22
  44. package/src/auth/services.ts +6 -3
  45. package/src/backup/backup-cli.ts +41 -2
  46. package/src/backup/backup-service.ts +38 -5
  47. package/src/backup/pg-tools.ts +96 -3
  48. package/src/cli-helpers.ts +70 -0
  49. package/src/cli.ts +44 -26
  50. package/src/collections/validate-relations.ts +15 -0
  51. package/src/connection.ts +73 -0
  52. package/src/data-transformer.ts +9 -3
  53. package/src/databasePoolManager.ts +5 -2
  54. package/src/schema/ensure-collection-tables.ts +81 -10
  55. package/src/schema/generate-drizzle-schema-logic.ts +35 -3
  56. package/src/schema/generate-postgres-ddl-logic.ts +50 -6
  57. package/src/schema/introspect-db-constraints.ts +385 -0
  58. package/src/schema/introspect-db-inference.ts +18 -8
  59. package/src/schema/introspect-db-logic.ts +385 -71
  60. package/src/schema/introspect-db-queries.ts +326 -0
  61. package/src/schema/introspect-db-structure.ts +670 -0
  62. package/src/schema/introspect-db-types.ts +56 -0
  63. package/src/schema/introspect-db.ts +37 -80
  64. package/src/security/policy-drift.test.ts +11 -3
  65. package/src/services/FetchService.ts +90 -14
  66. package/src/services/PersistService.ts +20 -6
  67. package/src/services/RelationService.ts +211 -45
  68. package/src/services/channel-bus/index.ts +0 -9
  69. package/src/services/collection-helpers.ts +69 -3
  70. package/src/services/dataService.ts +3 -1
  71. package/src/services/realtimeService.ts +3 -3
  72. package/src/services/row-pipeline.ts +1 -1
  73. package/src/utils/connection-string.ts +58 -0
  74. package/src/utils/drizzle-conditions.ts +31 -6
  75. package/src/utils/pg-error-utils.ts +19 -0
  76. package/src/websocket.ts +18 -9
  77. package/dist/backup-service-DH9kPg-E.js.map +0 -1
  78. package/dist/connection-B5Wndbr1.js.map +0 -1
  79. package/dist/ensure-collection-tables-DsDsNl6o.js.map +0 -1
  80. package/dist/src-DihrDFuP.js.map +0 -1
  81. package/dist/websocket-BKcGvILX.js.map +0 -1
@@ -0,0 +1,105 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ import "process";
3
+ __createRequire(import.meta.url);
4
+ //#region ../types/src/types/policy.ts
5
+ /**
6
+ * The id a request without a logged-in user reports as `auth.uid()`.
7
+ *
8
+ * A user-context request always sets `app.uid`: blank would read back as
9
+ * `NULL`, and `NULL` is how the trusted server context is recognised, so an
10
+ * anonymous visitor would be promoted to server privileges. The driver
11
+ * therefore substitutes this sentinel at the single chokepoint where the GUC
12
+ * is set.
13
+ *
14
+ * The consequence for policy authors is that **`auth.uid() IS NOT NULL` is a
15
+ * tautology on the user path** — it is true for anonymous visitors too. Use
16
+ * {@link policy.authenticated} to mean "signed in", and
17
+ * {@link policy.serverContext} to mean "the trusted server context". Do not
18
+ * hand-write the comparison: see {@link ANONYMOUS_USER_IDS} for why one
19
+ * literal is not enough.
20
+ *
21
+ * @group Models
22
+ */
23
+ var ANONYMOUS_USER_ID = "anonymous";
24
+ /**
25
+ * Every uid that has ever meant "nobody is signed in" — newest first.
26
+ *
27
+ * There are two because there were two. The types, the policy compiler, the
28
+ * JavaScript evaluator and the linter were all built on
29
+ * {@link ANONYMOUS_USER_ID}, while the request path scoped unauthenticated
30
+ * callers as `'anon'` — so `policy.authenticated()`, which compiled to
31
+ * `auth.uid() <> 'anonymous'`, was *true* for an anonymous visitor. The
32
+ * sanctioned way to write "signed in" granted to everyone, and the linter
33
+ * flagged the spelling that actually worked as a foreign convention.
34
+ *
35
+ * The request path now reports {@link ANONYMOUS_USER_ID}. `'anon'` stays here
36
+ * because policies outlive the server that generated them: a database still
37
+ * holding policies from before the fix, or a project whose server has not been
38
+ * upgraded yet, must not become a grant in either direction. Compile against
39
+ * this list, not against a single literal.
40
+ *
41
+ * No real user id is ever one of these, so a match is always "not signed in".
42
+ *
43
+ * @group Models
44
+ */
45
+ var ANONYMOUS_USER_IDS = [ANONYMOUS_USER_ID, "anon"];
46
+ /** @group Models */
47
+ var policy = {
48
+ true: () => ({ kind: "true" }),
49
+ false: () => ({ kind: "false" }),
50
+ and: (...operands) => ({
51
+ kind: "and",
52
+ operands
53
+ }),
54
+ or: (...operands) => ({
55
+ kind: "or",
56
+ operands
57
+ }),
58
+ not: (operand) => ({
59
+ kind: "not",
60
+ operand
61
+ }),
62
+ compare: (left, op, right) => ({
63
+ kind: "compare",
64
+ op,
65
+ left,
66
+ right
67
+ }),
68
+ rolesOverlap: (roles) => ({
69
+ kind: "rolesOverlap",
70
+ roles
71
+ }),
72
+ rolesContain: (roles) => ({
73
+ kind: "rolesContain",
74
+ roles
75
+ }),
76
+ authenticated: () => ({ kind: "authenticated" }),
77
+ serverContext: () => ({ kind: "serverContext" }),
78
+ existsIn: (args) => ({
79
+ kind: "existsIn",
80
+ collection: args.collection,
81
+ where: args.where
82
+ }),
83
+ raw: (sql) => ({
84
+ kind: "raw",
85
+ sql
86
+ }),
87
+ field: (name) => ({
88
+ kind: "field",
89
+ name
90
+ }),
91
+ outerField: (name) => ({
92
+ kind: "outerField",
93
+ name
94
+ }),
95
+ literal: (value) => ({
96
+ kind: "literal",
97
+ value
98
+ }),
99
+ authUid: () => ({ kind: "authUid" }),
100
+ authRoles: () => ({ kind: "authRoles" })
101
+ };
102
+ //#endregion
103
+ export { ANONYMOUS_USER_IDS as n, policy as r, ANONYMOUS_USER_ID as t };
104
+
105
+ //# sourceMappingURL=policy-CeA1JcxP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-CeA1JcxP.js","names":[],"sources":["../../types/src/types/policy.ts"],"sourcesContent":["/**\n * Structured, engine-agnostic policy expressions.\n *\n * A {@link PolicyExpression} is the single source of truth for a row-level\n * security condition. It is compiled to Postgres `USING`/`WITH CHECK` SQL\n * (authoritative enforcement) and independently evaluated in JavaScript (to\n * drive the admin UI, and — in future — to enforce on engines without native\n * RLS such as MongoDB). Because both the SQL and the JS decision derive from\n * the *same* expression, the UI matches database enforcement by construction —\n * no drift between two hand-written implementations.\n *\n * The only escape hatch that cannot be evaluated client-side is the\n * {@link RawPolicyExpression} node (`{ kind: \"raw\" }`): it preserves full\n * PostgreSQL power but, being arbitrary SQL, is treated as *unknown* by the\n * JavaScript evaluator (never silently allowed) and reflected exactly in the UI\n * via server-computed capability flags.\n *\n * @group Models\n */\nexport type PolicyExpression =\n | TruePolicyExpression\n | FalsePolicyExpression\n | AndPolicyExpression\n | OrPolicyExpression\n | NotPolicyExpression\n | ComparePolicyExpression\n | RolesOverlapPolicyExpression\n | RolesContainPolicyExpression\n | AuthenticatedPolicyExpression\n | ServerContextPolicyExpression\n | ExistsInPolicyExpression\n | RawPolicyExpression;\n\n/**\n * The id a request without a logged-in user reports as `auth.uid()`.\n *\n * A user-context request always sets `app.uid`: blank would read back as\n * `NULL`, and `NULL` is how the trusted server context is recognised, so an\n * anonymous visitor would be promoted to server privileges. The driver\n * therefore substitutes this sentinel at the single chokepoint where the GUC\n * is set.\n *\n * The consequence for policy authors is that **`auth.uid() IS NOT NULL` is a\n * tautology on the user path** — it is true for anonymous visitors too. Use\n * {@link policy.authenticated} to mean \"signed in\", and\n * {@link policy.serverContext} to mean \"the trusted server context\". Do not\n * hand-write the comparison: see {@link ANONYMOUS_USER_IDS} for why one\n * literal is not enough.\n *\n * @group Models\n */\nexport const ANONYMOUS_USER_ID = \"anonymous\";\n\n/**\n * Every uid that has ever meant \"nobody is signed in\" — newest first.\n *\n * There are two because there were two. The types, the policy compiler, the\n * JavaScript evaluator and the linter were all built on\n * {@link ANONYMOUS_USER_ID}, while the request path scoped unauthenticated\n * callers as `'anon'` — so `policy.authenticated()`, which compiled to\n * `auth.uid() <> 'anonymous'`, was *true* for an anonymous visitor. The\n * sanctioned way to write \"signed in\" granted to everyone, and the linter\n * flagged the spelling that actually worked as a foreign convention.\n *\n * The request path now reports {@link ANONYMOUS_USER_ID}. `'anon'` stays here\n * because policies outlive the server that generated them: a database still\n * holding policies from before the fix, or a project whose server has not been\n * upgraded yet, must not become a grant in either direction. Compile against\n * this list, not against a single literal.\n *\n * No real user id is ever one of these, so a match is always \"not signed in\".\n *\n * @group Models\n */\nexport const ANONYMOUS_USER_IDS: readonly string[] = [ANONYMOUS_USER_ID, \"anon\"];\n\n/**\n * Whether a uid stands for \"no one is signed in\", in any spelling rebase has\n * used. `null`/`undefined` is the trusted server context, not an anonymous\n * caller, and is therefore **not** anonymous — see {@link ANONYMOUS_USER_ID}.\n *\n * @group Models\n */\nexport function isAnonymousUid(uid: string | null | undefined): boolean {\n return typeof uid === \"string\" && ANONYMOUS_USER_IDS.includes(uid);\n}\n\n/** Always allows. Compiles to `true`. @group Models */\nexport interface TruePolicyExpression {\n kind: \"true\";\n}\n\n/** Always denies. Compiles to `false`. @group Models */\nexport interface FalsePolicyExpression {\n kind: \"false\";\n}\n\n/** Logical AND — every operand must pass. @group Models */\nexport interface AndPolicyExpression {\n kind: \"and\";\n operands: readonly PolicyExpression[];\n}\n\n/** Logical OR — at least one operand must pass. @group Models */\nexport interface OrPolicyExpression {\n kind: \"or\";\n operands: readonly PolicyExpression[];\n}\n\n/** Logical negation. @group Models */\nexport interface NotPolicyExpression {\n kind: \"not\";\n operand: PolicyExpression;\n}\n\n/** Comparison operators available to {@link ComparePolicyExpression}. @group Models */\nexport type PolicyCompareOperator = \"eq\" | \"neq\" | \"lt\" | \"lte\" | \"gt\" | \"gte\";\n\n/**\n * Compares two operands, e.g. `owner_id = auth.uid()`.\n * @group Models\n */\nexport interface ComparePolicyExpression {\n kind: \"compare\";\n op: PolicyCompareOperator;\n left: PolicyOperand;\n right: PolicyOperand;\n}\n\n/**\n * True when the user holds *at least one* of the given application roles.\n * Compiles to `string_to_array(auth.roles(), ',') && ARRAY[...]`.\n * @group Models\n */\nexport interface RolesOverlapPolicyExpression {\n kind: \"rolesOverlap\";\n roles: readonly string[];\n}\n\n/**\n * True when the user holds *all* of the given application roles.\n * Compiles to `string_to_array(auth.roles(), ',') @> ARRAY[...]`.\n * @group Models\n */\nexport interface RolesContainPolicyExpression {\n kind: \"rolesContain\";\n roles: readonly string[];\n}\n\n/**\n * True when a signed-in user is making the request. Compiles to\n * `auth.uid() IS NOT NULL AND auth.uid() <> 'anonymous'`.\n *\n * Both halves are load-bearing. `IS NOT NULL` excludes the server context;\n * the {@link ANONYMOUS_USER_ID} comparison excludes anonymous visitors, who\n * *do* carry a non-null `auth.uid()`. Checking only `IS NOT NULL` grants to\n * everyone — see {@link ANONYMOUS_USER_ID}.\n *\n * `policy.not(policy.authenticated())` therefore means \"anonymous visitor or\n * the server context\". To single out the server context, use\n * {@link ServerContextPolicyExpression}.\n * @group Models\n */\nexport interface AuthenticatedPolicyExpression {\n kind: \"authenticated\";\n}\n\n/**\n * True only in the trusted **server context** — the built-in flows that run\n * without a user (signup, migrations, `dataAsAdmin`) set no user GUC, so\n * `auth.uid()` is `NULL` for them and only for them. Compiles to\n * `auth.uid() IS NULL`.\n *\n * This is what lets the owner connection satisfy a policy even under FORCE RLS.\n * It is deliberately a primitive rather than `not(authenticated())`: the two\n * meant the same thing while `authenticated` ignored {@link ANONYMOUS_USER_ID},\n * and conflating them is what turns a server-only grant into an anonymous one.\n *\n * The JavaScript evaluator always returns `false` for this node — a client is\n * never the server context.\n * @group Models\n */\nexport interface ServerContextPolicyExpression {\n kind: \"serverContext\";\n}\n\n/**\n * Membership / relational access: true when at least one row exists in another\n * collection (a join/membership table) matching `where`. This is what lets you\n * scope reads to \"rows whose team the caller belongs to\" without an N+1\n * per-row lookup — it compiles to a single correlated `EXISTS` subquery.\n *\n * Inside `where`, {@link FieldPolicyOperand} (`policy.field`) references a column\n * of the joined collection, while {@link OuterFieldPolicyOperand}\n * (`policy.outerField`) references a column of the row being checked (the outer\n * table under RLS). Combine with {@link AuthUidPolicyOperand} to correlate to\n * the caller.\n *\n * @example\n * ```ts\n * // documents visible only to members of the document's team:\n * policy.existsIn({\n * collection: \"team_members\",\n * where: policy.and(\n * policy.compare(policy.field(\"team_id\"), \"eq\", policy.outerField(\"team_id\")),\n * policy.compare(policy.field(\"user_id\"), \"eq\", policy.authUid()),\n * ),\n * })\n * // → EXISTS (SELECT 1 FROM team_members _ex0\n * // WHERE _ex0.team_id = documents.team_id AND _ex0.user_id = auth.uid())\n * ```\n *\n * Postgres-authoritative: like {@link RawPolicyExpression}, the JavaScript\n * evaluator treats it as *unknown* (it cannot run a subquery client-side), so\n * enforcement is always the database's.\n * @group Models\n */\nexport interface ExistsInPolicyExpression {\n kind: \"existsIn\";\n /** Slug of the collection to search (the join / membership table). */\n collection: string;\n /** Condition evaluated against the joined collection's rows. */\n where: PolicyExpression;\n}\n\n/**\n * A raw PostgreSQL boolean expression — the full-power escape hatch.\n *\n * Columns can be referenced as `{column_name}`. This is Postgres-only and\n * **server-authoritative**: the JavaScript evaluator cannot evaluate arbitrary\n * SQL, so it treats this node as *unknown* rather than guessing.\n * @group Models\n */\nexport interface RawPolicyExpression {\n kind: \"raw\";\n sql: string;\n}\n\n/**\n * An operand referenced by a {@link ComparePolicyExpression}.\n * @group Models\n */\nexport type PolicyOperand =\n | FieldPolicyOperand\n | OuterFieldPolicyOperand\n | LiteralPolicyOperand\n | AuthUidPolicyOperand\n | AuthRolesPolicyOperand;\n\n/** A column value on the row being evaluated. @group Models */\nexport interface FieldPolicyOperand {\n kind: \"field\";\n /** The property/column name (resolved to its DB column when compiled). */\n name: string;\n}\n\n/**\n * A column value on the *outer* row when used inside {@link ExistsInPolicyExpression}\n * — i.e. the row the RLS policy is being evaluated for, referenced from within the\n * subquery. Outside an `existsIn` it is equivalent to {@link FieldPolicyOperand}.\n * @group Models\n */\nexport interface OuterFieldPolicyOperand {\n kind: \"outerField\";\n /** The property/column name on the outer collection. */\n name: string;\n}\n\n/** A constant value. @group Models */\nexport interface LiteralPolicyOperand {\n kind: \"literal\";\n value: string | number | boolean | null;\n}\n\n/** The current user's id — compiles to `auth.uid()`. @group Models */\nexport interface AuthUidPolicyOperand {\n kind: \"authUid\";\n}\n\n/**\n * The current user's roles as an array — compiles to\n * `string_to_array(auth.roles(), ',')`.\n * @group Models\n */\nexport interface AuthRolesPolicyOperand {\n kind: \"authRoles\";\n}\n\n// ── Constructor helpers ──────────────────────────────────────────────\n// Small, dependency-free builders so callers (and the desugaring in\n// `@rebasepro/common`) can assemble expressions without object-literal noise.\n\n/** @group Models */\nexport const policy = {\n true: (): TruePolicyExpression => ({ kind: \"true\" }),\n false: (): FalsePolicyExpression => ({ kind: \"false\" }),\n and: (...operands: readonly PolicyExpression[]): AndPolicyExpression => ({ kind: \"and\",\noperands: operands as PolicyExpression[] }),\n or: (...operands: readonly PolicyExpression[]): OrPolicyExpression => ({ kind: \"or\",\noperands: operands as PolicyExpression[] }),\n not: (operand: PolicyExpression): NotPolicyExpression => ({ kind: \"not\",\noperand }),\n compare: (left: PolicyOperand, op: PolicyCompareOperator, right: PolicyOperand): ComparePolicyExpression =>\n ({ kind: \"compare\",\nop,\nleft,\nright }),\n rolesOverlap: (roles: readonly string[]): RolesOverlapPolicyExpression => ({ kind: \"rolesOverlap\",\nroles: roles as string[] }),\n rolesContain: (roles: readonly string[]): RolesContainPolicyExpression => ({ kind: \"rolesContain\",\nroles: roles as string[] }),\n authenticated: (): AuthenticatedPolicyExpression => ({ kind: \"authenticated\" }),\n serverContext: (): ServerContextPolicyExpression => ({ kind: \"serverContext\" }),\n existsIn: (args: { collection: string; where: PolicyExpression }): ExistsInPolicyExpression =>\n ({ kind: \"existsIn\",\ncollection: args.collection,\nwhere: args.where }),\n raw: (sql: string): RawPolicyExpression => ({ kind: \"raw\",\nsql }),\n field: (name: string): FieldPolicyOperand => ({ kind: \"field\",\nname }),\n outerField: (name: string): OuterFieldPolicyOperand => ({ kind: \"outerField\",\nname }),\n literal: (value: string | number | boolean | null): LiteralPolicyOperand => ({ kind: \"literal\",\nvalue }),\n authUid: (): AuthUidPolicyOperand => ({ kind: \"authUid\" }),\n authRoles: (): AuthRolesPolicyOperand => ({ kind: \"authRoles\" })\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAmDA,IAAa,oBAAoB;;;;;;;;;;;;;;;;;;;;;;AAuBjC,IAAa,qBAAwC,CAAC,mBAAmB,MAAM;;AA2N/E,IAAa,SAAS;CAClB,aAAmC,EAAE,MAAM,OAAO;CAClD,cAAqC,EAAE,MAAM,QAAQ;CACrD,MAAM,GAAG,cAAgE;EAAE,MAAM;EAC3E;CAA+B;CACrC,KAAK,GAAG,cAA+D;EAAE,MAAM;EACzE;CAA+B;CACrC,MAAM,aAAoD;EAAE,MAAM;EACtE;CAAQ;CACJ,UAAU,MAAqB,IAA2B,WACrD;EAAE,MAAM;EACjB;EACA;EACA;CAAM;CACF,eAAe,WAA4D;EAAE,MAAM;EAChF;CAAkB;CACrB,eAAe,WAA4D;EAAE,MAAM;EAChF;CAAkB;CACrB,sBAAqD,EAAE,MAAM,gBAAgB;CAC7E,sBAAqD,EAAE,MAAM,gBAAgB;CAC7E,WAAW,UACN;EAAE,MAAM;EACjB,YAAY,KAAK;EACjB,OAAO,KAAK;CAAM;CACd,MAAM,SAAsC;EAAE,MAAM;EACxD;CAAI;CACA,QAAQ,UAAsC;EAAE,MAAM;EAC1D;CAAK;CACD,aAAa,UAA2C;EAAE,MAAM;EACpE;CAAK;CACD,UAAU,WAAmE;EAAE,MAAM;EACzF;CAAM;CACF,gBAAsC,EAAE,MAAM,UAAU;CACxD,kBAA0C,EAAE,MAAM,YAAY;AAClE"}
@@ -56,7 +56,7 @@ export interface ExistingSchema {
56
56
  constraints?: Set<string>;
57
57
  }
58
58
  export interface EnsureAction {
59
- kind: "create-enum" | "create-table" | "add-column" | "add-constraint";
59
+ kind: "create-enum" | "create-table" | "add-column" | "add-constraint" | "rename-column";
60
60
  /** Qualified target, for logging: `public.posts` or `public.posts.title`. */
61
61
  target: string;
62
62
  sql: string;
@@ -65,6 +65,26 @@ export interface EnsurePlan {
65
65
  actions: EnsureAction[];
66
66
  /** Every statement, in dependency order. Empty when the schema is current. */
67
67
  statements: string[];
68
+ /**
69
+ * Relation columns this plan is about to create where the table already
70
+ * carries the same column under its pre-singularization name.
71
+ *
72
+ * The reason this is reported rather than silently handled: the ensure is
73
+ * additive, so it would add `category_id` beside a populated
74
+ * `categorie_id` and the relation would then read the new, empty one. No
75
+ * statement fails, no table is missing, and the only symptom is relations
76
+ * resolving to nothing — which is indistinguishable from having no data.
77
+ */
78
+ legacyForeignKeys: LegacyForeignKey[];
79
+ }
80
+ /** A relation column whose old and new spellings both plausibly apply. */
81
+ export interface LegacyForeignKey {
82
+ /** `schema.table`. */
83
+ table: string;
84
+ /** The name the current rule derives, and what this plan would create. */
85
+ expected: string;
86
+ /** The name the old rule derived, which the table already has. */
87
+ legacy: string;
68
88
  }
69
89
  export interface EnsureOutcome extends EnsurePlan {
70
90
  /**
@@ -19,6 +19,15 @@ type ResolveCollection = (slug: string) => CollectionConfig | undefined;
19
19
  * multiple commands in one execute), while `db push` writes the joined string.
20
20
  */
21
21
  export declare const generatePolicyStatements: (collection: CollectionConfig, rule: SecurityRule, resolveCollection: ResolveCollection) => string[];
22
+ /**
23
+ * Single-quote escaping for a SQL string literal (PostgreSQL doubles the
24
+ * quote). Enum labels come straight from user-authored collection config, so a
25
+ * label like `it's` closes the literal early and the whole generated file stops
26
+ * parsing at the `CREATE TYPE`. Lives here rather than next to its other caller
27
+ * because ensure-collection-tables already imports from this module — the
28
+ * reverse would be a cycle.
29
+ */
30
+ export declare const quoteSqlLiteral: (value: string) => string;
22
31
  export declare const getSqlColumnType: (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]) => string;
23
32
  export declare const generatePostgresDdl: (collections: CollectionConfig[], options?: {
24
33
  includePolicies?: boolean;
@@ -52,6 +61,15 @@ export interface RelationalColumnPlan {
52
61
  type: string;
53
62
  /** Absent when the target collection is not part of this bundle. */
54
63
  foreignKey?: ForeignKeyPlan;
64
+ /**
65
+ * What this column would have been called before `generateForeignKeyName`
66
+ * learned to singularize — set only when the two differ and the column name
67
+ * is the derived default rather than one the author wrote.
68
+ *
69
+ * Carried so the boot-time ensure can notice a database provisioned under
70
+ * the old rule. It is never used to name anything.
71
+ */
72
+ legacyColumn?: string;
55
73
  }
56
74
  /** The table behind a many-to-many `through` relation. */
57
75
  export interface JunctionTablePlan {
@@ -61,6 +79,7 @@ export interface JunctionTablePlan {
61
79
  columns: {
62
80
  name: string;
63
81
  type: string;
82
+ legacyName?: string;
64
83
  }[];
65
84
  /** Both endpoint columns plus the composite primary key. */
66
85
  createTable: string;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Reading validation rules out of CHECK constraints.
3
+ *
4
+ * A CHECK constraint is the schema author stating a rule the database already
5
+ * enforces. Introspection has never read them, so a generated form let the user
6
+ * type a value the database was always going to reject — the rule was written
7
+ * down, in the schema, and the UI asked anyway.
8
+ *
9
+ * Everything here parses the *normalized* text `pg_get_constraintdef` produces,
10
+ * not what the author typed: Postgres re-renders the expression from its parse
11
+ * tree, so `CHECK (price > 0)` on a `numeric` column always comes back as
12
+ * `CHECK ((price > (0)::numeric))`. That normalization is what makes a
13
+ * string-level parser tractable — the input is generated, and the shapes are
14
+ * few.
15
+ *
16
+ * Constraints this cannot read are skipped in full. A partially-understood
17
+ * constraint is worse than an unread one: it would produce validation that
18
+ * *narrows* differently from the database, so a value the UI accepts still
19
+ * fails on write, or — worse — a value the database allows is refused in the
20
+ * form with no way to see why.
21
+ *
22
+ * Pure module: no I/O, no logging.
23
+ */
24
+ import type { CheckConstraintRow } from "./introspect-db-logic";
25
+ /** What a table's CHECK constraints say about one column. */
26
+ export interface ColumnCheckFacts {
27
+ /** Allowed values, from `IN (…)` / `= ANY (ARRAY[…])` / `= 'literal'`. */
28
+ enumValues?: string[];
29
+ /** `x >= n` */
30
+ min?: number;
31
+ /** `x <= n` */
32
+ max?: number;
33
+ /** `x > n` */
34
+ moreThan?: number;
35
+ /** `x < n` */
36
+ lessThan?: number;
37
+ /** `length(x) >= n` */
38
+ lengthMin?: number;
39
+ /** `length(x) <= n` */
40
+ lengthMax?: number;
41
+ }
42
+ /** Per-table, per-column facts. */
43
+ export type CheckFactsByTable = Map<string, Map<string, ColumnCheckFacts>>;
44
+ /** Removes one layer of wrapping parentheses, repeatedly, when balanced. */
45
+ export declare function unwrapParens(input: string): string;
46
+ /** Drops trailing `::type` casts, including array and quoted forms. */
47
+ export declare function stripCasts(input: string): string;
48
+ /**
49
+ * Reads one constraint definition into per-column facts.
50
+ *
51
+ * Returns an empty map for anything not understood — including a constraint
52
+ * that is understood but spans two columns (`start_date < end_date`), which no
53
+ * per-property validation rule can express.
54
+ */
55
+ export declare function parseCheckDefinition(definition: string): Map<string, ColumnCheckFacts>;
56
+ /** Merges every readable CHECK in the schema into per-table, per-column facts. */
57
+ export declare function parseCheckConstraints(rows: CheckConstraintRow[]): CheckFactsByTable;
@@ -1,5 +1,10 @@
1
+ import { mapPgType } from "./introspect-db-types";
2
+ import type { CheckFactsByTable } from "./introspect-db-constraints";
3
+ import type { TableClassification } from "./introspect-db-structure";
1
4
  export interface TableRow {
2
5
  table_name: string;
6
+ /** True for the parent of a partitioned table (`relkind = 'p'`). */
7
+ is_partitioned?: boolean;
3
8
  }
4
9
  export interface TableColumn {
5
10
  table_name: string;
@@ -9,6 +14,18 @@ export interface TableColumn {
9
14
  is_nullable: string;
10
15
  column_default: string | null;
11
16
  atttypmod: number | null;
17
+ /** 1-based position in the table, as declared. */
18
+ ordinal_position?: number;
19
+ /** `"ALWAYS"` for a generated column, `"NEVER"` otherwise. */
20
+ is_generated?: string;
21
+ /** `"YES"` for an identity column. */
22
+ is_identity?: string;
23
+ /** `"ALWAYS"` or `"BY DEFAULT"` on an identity column. */
24
+ identity_generation?: string | null;
25
+ /** The declared `varchar(n)` / `char(n)` bound, if any. */
26
+ character_maximum_length?: number | null;
27
+ numeric_precision?: number | null;
28
+ numeric_scale?: number | null;
12
29
  }
13
30
  export interface EnumValue {
14
31
  enum_name: string;
@@ -24,6 +41,53 @@ export interface ForeignKeyRow {
24
41
  column_name: string;
25
42
  foreign_table_name: string;
26
43
  foreign_column_name: string;
44
+ /** Name of the FK constraint — the only way to tell composite keys apart. */
45
+ constraint_name?: string;
46
+ /** 1-based position of this column within its constraint. */
47
+ ordinal?: number;
48
+ /** `"CASCADE"`, `"RESTRICT"`, `"SET NULL"`, `"SET DEFAULT"`, `"NO ACTION"`. */
49
+ delete_rule?: string;
50
+ }
51
+ /** A unique constraint or unique index, as an ordered column list. */
52
+ export interface UniqueConstraintRow {
53
+ table_name: string;
54
+ constraint_name: string;
55
+ column_names: string[];
56
+ }
57
+ /** A CHECK constraint, as `pg_get_constraintdef` renders it. */
58
+ export interface CheckConstraintRow {
59
+ table_name: string;
60
+ constraint_name: string;
61
+ definition: string;
62
+ }
63
+ /** A `COMMENT ON TABLE` (null `column_name`) or `COMMENT ON COLUMN`. */
64
+ export interface CommentRow {
65
+ table_name: string;
66
+ column_name: string | null;
67
+ comment: string;
68
+ }
69
+ /**
70
+ * Everything one introspection run reads from the database.
71
+ *
72
+ * Passed around as one value so a new signal means a new field here rather than
73
+ * a new parameter on every function between the query and the generator — the
74
+ * shape `generateCollectionFile` had grown to seven positional arguments by.
75
+ */
76
+ export interface SchemaMetadata {
77
+ schema: string;
78
+ tables: TableRow[];
79
+ columns: TableColumn[];
80
+ enumValues: EnumValue[];
81
+ pks: PrimaryKeyRow[];
82
+ fks: ForeignKeyRow[];
83
+ uniques: UniqueConstraintRow[];
84
+ checks: CheckConstraintRow[];
85
+ comments: CommentRow[];
86
+ /**
87
+ * Row counts for the tables that needed one, capped — see `countRowsUpTo`.
88
+ * Absent for every table introspection never had a reason to count.
89
+ */
90
+ rowCounts: Record<string, number>;
27
91
  }
28
92
  export interface TableMeta {
29
93
  name: string;
@@ -38,12 +102,23 @@ export declare function singularize(word: string): string;
38
102
  */
39
103
  export declare function toCollectionVarName(tableName: string): string;
40
104
  export declare function getIconForTable(tableName: string): string;
41
- /**
42
- * Map a PostgreSQL data type to a Rebase property type.
43
- */
44
- export declare function mapPgType(dataType: string): string;
105
+ export { mapPgType };
45
106
  export declare function buildEnumMap(enumValues: EnumValue[]): Map<string, string[]>;
46
107
  export declare function buildTablesMap(tables: TableRow[], columns: TableColumn[], pks: PrimaryKeyRow[], fks: ForeignKeyRow[]): Map<string, TableMeta>;
108
+ /**
109
+ * Join tables, identified by column name.
110
+ *
111
+ * Superseded for the CLI by `classifyTables` in `./introspect-db-structure`,
112
+ * which asks the database instead: two single-column keys, unique together, no
113
+ * payload column, nothing referencing the table. This rule folds away
114
+ * `northwind.order_details` — which has the key shape and carries unit price,
115
+ * quantity and discount — because it recognises `id`, `created_at` and
116
+ * `updated_at` by name and calls everything else a foreign key.
117
+ *
118
+ * Still used by `./introspect-runtime`, which builds collections in memory from
119
+ * a narrower set of catalog queries and has no unique-constraint or row-count
120
+ * data to reason with.
121
+ */
47
122
  export declare function identifyJoinTables(tablesMap: Map<string, TableMeta>): Set<string>;
48
123
  /**
49
124
  * Property metadata used to compute display priority.
@@ -93,11 +168,25 @@ export interface GeneratedFile {
93
168
  fileName: string;
94
169
  content: string;
95
170
  }
171
+ /**
172
+ * The structural analysis a run can hand the generator.
173
+ *
174
+ * Optional in full, and the generator degrades to exactly its previous output
175
+ * without it. That is not politeness towards old callers: three existing test
176
+ * suites and the `rebase init` scaffold path build a `TableMeta` by hand and
177
+ * have no database to read constraints or row counts from, and they must keep
178
+ * producing a valid collection.
179
+ */
180
+ export interface GenerationContext {
181
+ metadata?: SchemaMetadata;
182
+ classifications?: Map<string, TableClassification>;
183
+ checkFacts?: CheckFactsByTable;
184
+ }
96
185
  /**
97
186
  * Generate the full TypeScript file content for a single collection.
98
187
  * Pure function — no I/O.
99
188
  */
100
- export declare function generateCollectionFile(tableName: string, meta: TableMeta, allFks: ForeignKeyRow[], joinTables: Set<string>, tablesMap: Map<string, TableMeta>, enumMap: Map<string, string[]>, sampleData?: Record<string, unknown>[]): string;
189
+ export declare function generateCollectionFile(tableName: string, meta: TableMeta, allFks: ForeignKeyRow[], joinTables: Set<string>, tablesMap: Map<string, TableMeta>, enumMap: Map<string, string[]>, sampleData?: Record<string, unknown>[], context?: GenerationContext): string;
101
190
  /**
102
191
  * Generate the content for an index.ts file that re-exports all collections.
103
192
  */
@@ -0,0 +1,119 @@
1
+ /**
2
+ * The catalog queries introspection runs, and the shape they produce.
3
+ *
4
+ * These live apart from {@link ./introspect-db.ts} (the CLI entry point) on
5
+ * purpose: the fixture-capture script and the live e2e read a database through
6
+ * this module too, so what the tests see is what the CLI sees. A query that
7
+ * only exists inside the CLI's `main()` can only be tested by running the CLI.
8
+ *
9
+ * No side effects beyond `SELECT`. Introspection reads a database it does not
10
+ * own — it must never `ANALYZE`, create a temp table, or otherwise write.
11
+ */
12
+ import type { SchemaMetadata } from "./introspect-db-logic";
13
+ /** The subset of `pg.Client` this module needs, so tests can pass a fake. */
14
+ export interface QueryableClient {
15
+ query<R = Record<string, unknown>>(text: string, values?: unknown[]): Promise<{
16
+ rows: R[];
17
+ }>;
18
+ }
19
+ /**
20
+ * Base tables, excluding partitions.
21
+ *
22
+ * `relispartition` is the reason this reads `pg_class` rather than
23
+ * `information_schema.tables`, which reports every partition as a base table of
24
+ * its own. Pagila partitions `payment` by month; introspected through
25
+ * information_schema it yields `payment` plus 26 near-identical
26
+ * `payment_p2022_*` collections, each with its own nav entry.
27
+ *
28
+ * `relkind = 'p'` (the partitioned parent) is included and `'r'` partitions are
29
+ * dropped, which is the right way round: the parent is the queryable table.
30
+ */
31
+ export declare 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";
32
+ /**
33
+ * Columns, with the catalog facts that say a column is not the user's to edit.
34
+ *
35
+ * `is_generated`/`is_identity` are read for that reason: a generated column
36
+ * rejects any write, so a form that offers it is offering a field that always
37
+ * errors. `character_maximum_length` is the declared `varchar(n)` bound —
38
+ * already enforced by the database, and free to surface as validation.
39
+ */
40
+ export declare 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.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";
41
+ export declare 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";
42
+ /**
43
+ * Primary key columns, in key order.
44
+ *
45
+ * `ORDER BY k.ord` matters for composite keys: the position of a column inside
46
+ * the key is what tells a two-column PK made of two foreign keys apart from an
47
+ * ordinary composite key, and `= ANY(i.indkey)` (the shape this replaced)
48
+ * returns catalog order instead.
49
+ */
50
+ export declare 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";
51
+ /**
52
+ * Foreign keys, one row per referencing column, with the delete rule.
53
+ *
54
+ * Read from `pg_constraint` rather than through
55
+ * `information_schema.constraint_column_usage`, which does not preserve the
56
+ * pairing on a composite foreign key: it reports the cross product of the
57
+ * constraint's columns, so a two-column FK comes back as four rows, two of them
58
+ * pairing the wrong columns. `unnest(...) WITH ORDINALITY` on both sides joined
59
+ * on the ordinal keeps each referencing column with the column it references.
60
+ *
61
+ * `confdeltype` is carried because `ON DELETE CASCADE` is the schema author
62
+ * saying, in the database, that the child cannot outlive the parent — the one
63
+ * unambiguous declaration of ownership a schema contains.
64
+ *
65
+ * Constraints declared on a *partition* are attributed to the partition root,
66
+ * because that is the table this run generates a collection for. Pagila declares
67
+ * `payment`'s three foreign keys on each monthly partition and none on the
68
+ * parent, so reading `pg_constraint` at face value gives the `payment`
69
+ * collection no relations at all. `pick = 1` then keeps one row per logical key,
70
+ * preferring the root's own constraint when it has one, so a schema that
71
+ * declares the key on the parent *and* inherits it onto 26 partitions does not
72
+ * yield 27 copies of the same relation.
73
+ */
74
+ export declare 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";
75
+ /**
76
+ * Unique constraints and unique indexes, as ordered column lists.
77
+ *
78
+ * Indexes rather than constraints alone, because `CREATE UNIQUE INDEX` and
79
+ * `ADD CONSTRAINT ... UNIQUE` produce the same guarantee and schemas use both.
80
+ * Partial indexes (`indpred IS NOT NULL`) are excluded: they promise uniqueness
81
+ * only over the rows matching their predicate, which is not the promise a
82
+ * `validation: { unique: true }` field makes.
83
+ */
84
+ export declare 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";
85
+ /**
86
+ * CHECK constraints, as the source text Postgres reproduces them from.
87
+ *
88
+ * `pg_get_constraintdef` output is normalized by the server — the parser in
89
+ * {@link ./introspect-db-constraints.ts} reads that normalized form, not
90
+ * whatever the author typed.
91
+ */
92
+ export declare 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";
93
+ /**
94
+ * `COMMENT ON TABLE` / `COMMENT ON COLUMN`, which is documentation the author
95
+ * already wrote and which nothing downstream has ever read.
96
+ */
97
+ export declare 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";
98
+ /**
99
+ * Counts a table's rows, stopping once the answer can only be "more than
100
+ * `limit`".
101
+ *
102
+ * Introspection asks this to tell a small reference table from a table that
103
+ * merely looks like one (see `classifyTables`). `reltuples` would answer for
104
+ * free but lies on a table that has never been analyzed — which is every table
105
+ * in a database restored from a dump, i.e. exactly the case introspection meets
106
+ * — and reports -1 there rather than an error, so the lie is silent.
107
+ *
108
+ * The subquery's LIMIT caps the work at `limit + 1` rows however large the
109
+ * table is, so this stays cheap on a table with a billion rows.
110
+ */
111
+ export declare function countRowsUpTo(client: QueryableClient, schema: string, table: string, limit: number): Promise<number>;
112
+ /**
113
+ * Reads everything the generator needs, in one pass.
114
+ *
115
+ * `rowCounts` is deliberately absent here: it costs a query per table and only
116
+ * a handful of tables can possibly need it. The caller fills it in for the
117
+ * candidates `lookupCandidates()` names.
118
+ */
119
+ export declare function readSchemaMetadata(client: QueryableClient, schema: string): Promise<SchemaMetadata>;