@rebasepro/server-postgres 0.12.1-canary.gf5f1d39 → 0.13.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.
- package/dist/PostgresBackendDriver.d.ts +1 -1
- package/dist/PostgresBootstrapper.d.ts +25 -1
- package/dist/auth/services.d.ts +21 -0
- package/dist/backup/backup-service.d.ts +10 -1
- package/dist/backup/pg-tools.d.ts +47 -0
- package/dist/backup-service-CD8o_1Sl.js +8999 -0
- package/dist/backup-service-CD8o_1Sl.js.map +1 -0
- package/dist/cli-helpers.d.ts +39 -0
- package/dist/connection-BuZ97wsr.js +250 -0
- package/dist/connection-BuZ97wsr.js.map +1 -0
- package/dist/connection.d.ts +42 -0
- package/dist/ensure-collection-policies-ViG8XiPn.js +57 -0
- package/dist/ensure-collection-policies-ViG8XiPn.js.map +1 -0
- package/dist/ensure-collection-tables-CBQdOETu.js +650 -0
- package/dist/ensure-collection-tables-CBQdOETu.js.map +1 -0
- package/dist/index.es.js +1231 -10031
- package/dist/index.es.js.map +1 -1
- package/dist/policy-CeA1JcxP.js +105 -0
- package/dist/policy-CeA1JcxP.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +83 -144
- package/dist/schema/ensure-collection-policies.d.ts +60 -0
- package/dist/schema/ensure-collection-tables.d.ts +44 -2
- package/dist/schema/generate-postgres-ddl-logic.d.ts +135 -1
- package/dist/schema/introspect-db-constraints.d.ts +57 -0
- package/dist/schema/introspect-db-logic.d.ts +94 -5
- package/dist/schema/introspect-db-queries.d.ts +119 -0
- package/dist/schema/introspect-db-structure.d.ts +263 -0
- package/dist/schema/introspect-db-types.d.ts +11 -0
- package/dist/services/FetchService.d.ts +4 -1
- package/dist/services/RelationService.d.ts +24 -1
- package/dist/services/channel-bus/index.d.ts +1 -7
- package/dist/services/collection-helpers.d.ts +36 -2
- package/dist/services/dataService.d.ts +3 -1
- package/dist/services/row-pipeline.d.ts +1 -1
- package/dist/{src-BbFOPJ1S.js → src-DlPBctw_.js} +299 -173
- package/dist/src-DlPBctw_.js.map +1 -0
- package/dist/{src-Zqwaw3P5.js → src-DoU9yPqq.js} +3 -159
- package/dist/src-DoU9yPqq.js.map +1 -0
- package/dist/utils/connection-string.d.ts +29 -0
- package/dist/utils/drizzle-conditions.d.ts +5 -4
- package/dist/utils/pg-error-utils.d.ts +35 -0
- package/dist/websocket-B2LsrINK.js +530 -0
- package/dist/websocket-B2LsrINK.js.map +1 -0
- package/package.json +14 -14
- package/src/PostgresAdapter.ts +21 -2
- package/src/PostgresBackendDriver.ts +4 -0
- package/src/PostgresBootstrapper.ts +192 -33
- package/src/auth/ensure-tables.ts +164 -9
- package/src/auth/services.ts +24 -2
- package/src/backup/backup-cli.ts +41 -2
- package/src/backup/backup-service.ts +38 -5
- package/src/backup/pg-tools.ts +96 -3
- package/src/cli-helpers.ts +70 -0
- package/src/cli.ts +44 -26
- package/src/collections/validate-relations.ts +15 -0
- package/src/connection.ts +73 -0
- package/src/data-transformer.ts +9 -3
- package/src/databasePoolManager.ts +5 -2
- package/src/schema/auth-schema.ts +30 -19
- package/src/schema/ensure-collection-policies.ts +105 -0
- package/src/schema/ensure-collection-tables.test.ts +105 -9
- package/src/schema/ensure-collection-tables.ts +220 -32
- package/src/schema/generate-drizzle-schema-logic.ts +42 -6
- package/src/schema/generate-postgres-ddl-logic.ts +382 -19
- package/src/schema/introspect-db-constraints.ts +385 -0
- package/src/schema/introspect-db-inference.ts +18 -8
- package/src/schema/introspect-db-logic.ts +385 -71
- package/src/schema/introspect-db-queries.ts +326 -0
- package/src/schema/introspect-db-structure.ts +670 -0
- package/src/schema/introspect-db-types.ts +56 -0
- package/src/schema/introspect-db.ts +37 -80
- package/src/schema/introspect-runtime.test.ts +56 -8
- package/src/schema/introspect-runtime.ts +31 -9
- package/src/security/policy-drift.test.ts +11 -3
- package/src/services/BranchService.ts +66 -28
- package/src/services/FetchService.ts +90 -14
- package/src/services/PersistService.ts +20 -6
- package/src/services/RelationService.ts +249 -48
- package/src/services/channel-bus/index.ts +0 -9
- package/src/services/collection-helpers.ts +69 -3
- package/src/services/dataService.ts +3 -1
- package/src/services/realtimeService.ts +3 -3
- package/src/services/row-pipeline.ts +1 -1
- package/src/utils/connection-string.ts +58 -0
- package/src/utils/drizzle-conditions.ts +31 -6
- package/src/utils/pg-error-utils.ts +65 -0
- package/src/websocket.ts +18 -9
- package/dist/chunk-DSJWtz9O.js +0 -40
- package/dist/ensure-collection-tables-CNTcZGvn.js +0 -304
- package/dist/ensure-collection-tables-CNTcZGvn.js.map +0 -1
- package/dist/src-BbFOPJ1S.js.map +0 -1
- package/dist/src-Zqwaw3P5.js.map +0 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applying a bundle's RLS policies to a database at boot, idempotently.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* {@link ensureCollectionTables} creates the collection *tables* a managed
|
|
7
|
+
* runtime boots against, but a table with row-level security disabled and no
|
|
8
|
+
* policies is not servable: authenticated requests run as the restricted
|
|
9
|
+
* `rebase_user` role, so a read with no `SELECT` policy returns nothing (a
|
|
10
|
+
* public collection answered 401) and a write with no `INSERT`/`UPDATE` policy
|
|
11
|
+
* is denied. The policies live in the collections' `securityRules`; nothing at
|
|
12
|
+
* boot applied them. `rebase db push` does — but it drives Atlas against a
|
|
13
|
+
* local `DATABASE_URL`, and a managed tenant's database is reachable only from
|
|
14
|
+
* inside the cluster, by the runtime that is already connected to it. So the
|
|
15
|
+
* runtime is the only thing that *can* apply them, and this is where it does.
|
|
16
|
+
*
|
|
17
|
+
* ## Why this is safe to run on every boot
|
|
18
|
+
*
|
|
19
|
+
* Every statement is idempotent: `ENABLE ROW LEVEL SECURITY` is a no-op once
|
|
20
|
+
* enabled, and each policy is a `DROP POLICY IF EXISTS` immediately followed by
|
|
21
|
+
* a `CREATE POLICY`, so re-applying asserts exactly the declared state. It adds
|
|
22
|
+
* and replaces; it never drops data. (It does not *reconcile* — a policy a
|
|
23
|
+
* previous push left behind under an old name is not removed here; that stays a
|
|
24
|
+
* `db push` / `db migrate` concern, alongside destructive schema changes.)
|
|
25
|
+
*
|
|
26
|
+
* Unlike table creation, a failure here is not fatal: RLS stays enabled, so a
|
|
27
|
+
* table whose policies could not be applied fails **closed** (denies) rather
|
|
28
|
+
* than leaking rows. One collection's policy failing (e.g. a rule that
|
|
29
|
+
* references a table a real migration has not created yet) must not crash-loop
|
|
30
|
+
* the whole deployment and take the other collections' working routes down with
|
|
31
|
+
* it. Failures are reported loudly and per-table so the operator can see
|
|
32
|
+
* exactly which collection is not yet servable and why.
|
|
33
|
+
*/
|
|
34
|
+
import { type CollectionConfig } from "@rebasepro/types";
|
|
35
|
+
import { type Queryable } from "./ensure-collection-tables";
|
|
36
|
+
export interface PolicyEnsureResult {
|
|
37
|
+
/** `CREATE POLICY` statements that ran successfully. */
|
|
38
|
+
policiesApplied: number;
|
|
39
|
+
/** Tables that had RLS enabled. */
|
|
40
|
+
tablesSecured: number;
|
|
41
|
+
/** Declared tables absent from the database — left to a real migration. */
|
|
42
|
+
skipped: {
|
|
43
|
+
table: string;
|
|
44
|
+
reason: string;
|
|
45
|
+
}[];
|
|
46
|
+
/** Tables whose RLS could not be fully applied (fail closed). */
|
|
47
|
+
failures: {
|
|
48
|
+
table: string;
|
|
49
|
+
error: string;
|
|
50
|
+
}[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Bring the declared collections' RLS policies up to date. Returns what it did.
|
|
54
|
+
*
|
|
55
|
+
* Only tables that already exist are touched: the boot-time table creator runs
|
|
56
|
+
* first, so anything still missing is a table this additive path is not allowed
|
|
57
|
+
* to create (a junction, or a relation left to a migration). Enabling RLS on a
|
|
58
|
+
* non-existent table would error, so those are recorded as skipped, not failed.
|
|
59
|
+
*/
|
|
60
|
+
export declare function ensureCollectionPolicies(client: Queryable, collections: CollectionConfig[], log?: (message: string) => void): Promise<PolicyEnsureResult>;
|
|
@@ -46,9 +46,17 @@ export interface ExistingSchema {
|
|
|
46
46
|
tables: Map<string, Set<string>>;
|
|
47
47
|
/** `schema.typename` of every enum type that already exists. */
|
|
48
48
|
enums: Set<string>;
|
|
49
|
+
/**
|
|
50
|
+
* `schema.table.constraint` of every constraint that already exists.
|
|
51
|
+
*
|
|
52
|
+
* Optional so a caller that only cares about tables can still build one by
|
|
53
|
+
* hand; absent is read as "none known", which at worst re-attempts a
|
|
54
|
+
* constraint that then fails harmlessly as a duplicate.
|
|
55
|
+
*/
|
|
56
|
+
constraints?: Set<string>;
|
|
49
57
|
}
|
|
50
58
|
export interface EnsureAction {
|
|
51
|
-
kind: "create-enum" | "create-table" | "add-column";
|
|
59
|
+
kind: "create-enum" | "create-table" | "add-column" | "add-constraint" | "rename-column";
|
|
52
60
|
/** Qualified target, for logging: `public.posts` or `public.posts.title`. */
|
|
53
61
|
target: string;
|
|
54
62
|
sql: string;
|
|
@@ -57,6 +65,40 @@ export interface EnsurePlan {
|
|
|
57
65
|
actions: EnsureAction[];
|
|
58
66
|
/** Every statement, in dependency order. Empty when the schema is current. */
|
|
59
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;
|
|
88
|
+
}
|
|
89
|
+
export interface EnsureOutcome extends EnsurePlan {
|
|
90
|
+
/**
|
|
91
|
+
* Constraints that could not be added — always non-fatal.
|
|
92
|
+
*
|
|
93
|
+
* A foreign key can only fail on data that already violates it, and the
|
|
94
|
+
* column it would police exists either way, so the collection still serves.
|
|
95
|
+
* Refusing to boot over one would turn a pre-existing data problem into an
|
|
96
|
+
* outage. Reported loudly instead.
|
|
97
|
+
*/
|
|
98
|
+
failures: {
|
|
99
|
+
target: string;
|
|
100
|
+
error: string;
|
|
101
|
+
}[];
|
|
60
102
|
}
|
|
61
103
|
/**
|
|
62
104
|
* Decide what to add. Pure — the caller supplies what exists and runs the result.
|
|
@@ -76,4 +118,4 @@ export declare function readExistingSchema(client: Queryable, schemas: string[])
|
|
|
76
118
|
* cannot be added, say) should not roll back the tables that were created fine.
|
|
77
119
|
* The error is surfaced with the statement that caused it.
|
|
78
120
|
*/
|
|
79
|
-
export declare function ensureCollectionTables(client: Queryable, collections: CollectionConfig[], log?: (message: string) => void): Promise<
|
|
121
|
+
export declare function ensureCollectionTables(client: Queryable, collections: CollectionConfig[], log?: (message: string) => void): Promise<EnsureOutcome>;
|
|
@@ -1,8 +1,142 @@
|
|
|
1
|
-
import { CollectionConfig, Property } from "@rebasepro/types";
|
|
1
|
+
import { CollectionConfig, Property, SecurityRule } from "@rebasepro/types";
|
|
2
2
|
export declare const resolveColumnName: (propName: string, prop?: Property | null) => string;
|
|
3
|
+
export declare const getPrimaryKeyProp: (collection: CollectionConfig) => {
|
|
4
|
+
name: string;
|
|
5
|
+
type: "string" | "number";
|
|
6
|
+
isUuid: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare const isNumericId: (collection: CollectionConfig) => boolean;
|
|
9
|
+
export declare const getPrimaryKeyName: (collection: CollectionConfig) => string;
|
|
3
10
|
export declare const isIdProperty: (propName: string, prop: Property, collection: CollectionConfig) => boolean;
|
|
11
|
+
type ResolveCollection = (slug: string) => CollectionConfig | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* The individual SQL statements a single security rule compiles to: a
|
|
14
|
+
* `DROP POLICY IF EXISTS` / `CREATE POLICY` pair per operation, each a complete
|
|
15
|
+
* statement (terminated by `;`, no trailing newline).
|
|
16
|
+
*
|
|
17
|
+
* This is the primitive the boot-time RLS applier runs one statement at a time
|
|
18
|
+
* (the runtime's DB handle speaks the extended query protocol, which forbids
|
|
19
|
+
* multiple commands in one execute), while `db push` writes the joined string.
|
|
20
|
+
*/
|
|
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;
|
|
4
31
|
export declare const getSqlColumnType: (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]) => string;
|
|
5
32
|
export declare const generatePostgresDdl: (collections: CollectionConfig[], options?: {
|
|
6
33
|
includePolicies?: boolean;
|
|
7
34
|
}) => Promise<string>;
|
|
35
|
+
/** The RLS statements one declared collection's table needs, ready to run. */
|
|
36
|
+
/**
|
|
37
|
+
* A foreign key, as both its parts and the statement that creates it.
|
|
38
|
+
*
|
|
39
|
+
* `ALTER TABLE … ADD CONSTRAINT` has no `IF NOT EXISTS`, so a caller applying
|
|
40
|
+
* these has to skip by name — hence the name is a field and not only a substring
|
|
41
|
+
* of the SQL.
|
|
42
|
+
*/
|
|
43
|
+
export interface ForeignKeyPlan {
|
|
44
|
+
constraintName: string;
|
|
45
|
+
schema: string;
|
|
46
|
+
/** Bare table name, no schema prefix. */
|
|
47
|
+
table: string;
|
|
48
|
+
column: string;
|
|
49
|
+
targetSchema: string;
|
|
50
|
+
targetTable: string;
|
|
51
|
+
targetColumn: string;
|
|
52
|
+
sql: string;
|
|
53
|
+
}
|
|
54
|
+
/** A column a `relation` or `reference` property owns on its own table. */
|
|
55
|
+
export interface RelationalColumnPlan {
|
|
56
|
+
schema: string;
|
|
57
|
+
/** Bare table name, no schema prefix. */
|
|
58
|
+
table: string;
|
|
59
|
+
column: string;
|
|
60
|
+
/** Postgres type, exactly as the DDL generator declares it. */
|
|
61
|
+
type: string;
|
|
62
|
+
/** Absent when the target collection is not part of this bundle. */
|
|
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;
|
|
73
|
+
}
|
|
74
|
+
/** The table behind a many-to-many `through` relation. */
|
|
75
|
+
export interface JunctionTablePlan {
|
|
76
|
+
schema: string;
|
|
77
|
+
/** Bare table name, no schema prefix. */
|
|
78
|
+
table: string;
|
|
79
|
+
columns: {
|
|
80
|
+
name: string;
|
|
81
|
+
type: string;
|
|
82
|
+
legacyName?: string;
|
|
83
|
+
}[];
|
|
84
|
+
/** Both endpoint columns plus the composite primary key. */
|
|
85
|
+
createTable: string;
|
|
86
|
+
foreignKeys: ForeignKeyPlan[];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The FK columns the declared collections own — one entry per `relation`
|
|
90
|
+
* (`belongsTo` side) or `reference` property.
|
|
91
|
+
*
|
|
92
|
+
* Split out of {@link generatePostgresDdl} so the boot-time schema ensure can
|
|
93
|
+
* create the same columns with the same names, types and constraints. Before
|
|
94
|
+
* this it skipped them outright, which was survivable only because `db push`
|
|
95
|
+
* always followed; on a managed tenant nothing follows, so a table arrived
|
|
96
|
+
* without the column its own collection reads and wrote 400 on every insert.
|
|
97
|
+
*
|
|
98
|
+
* A relation whose target is not in the bundle yields no column at all (the
|
|
99
|
+
* generator returns early on an unresolvable target); a `reference` whose target
|
|
100
|
+
* is unknown yields the column without a constraint. Both mirror the generator
|
|
101
|
+
* exactly — a divergence here is a schema fork between boot and `db push`.
|
|
102
|
+
*/
|
|
103
|
+
export declare const planRelationalColumns: (collections: CollectionConfig[]) => RelationalColumnPlan[];
|
|
104
|
+
/**
|
|
105
|
+
* The junction tables a bundle's many-to-many relations imply.
|
|
106
|
+
*
|
|
107
|
+
* Derived from {@link resolveJunctionSpecs}, the same source the junction RLS
|
|
108
|
+
* comes from, so a table created here always has policies planned for it — a
|
|
109
|
+
* junction with row-level security left off is readable and writable by every
|
|
110
|
+
* signed-in user, which is why the two must ship together.
|
|
111
|
+
*/
|
|
112
|
+
export declare const planJunctionTables: (collections: CollectionConfig[]) => JunctionTablePlan[];
|
|
113
|
+
export interface CollectionPolicyPlan {
|
|
114
|
+
/** The table's schema (e.g. `public`, `rebase`). */
|
|
115
|
+
schema: string;
|
|
116
|
+
/** The bare table name, no schema prefix. */
|
|
117
|
+
table: string;
|
|
118
|
+
/** `schema.table` — matches the keys `readExistingSchema` returns. */
|
|
119
|
+
qualified: string;
|
|
120
|
+
/** `ALTER TABLE … ENABLE ROW LEVEL SECURITY;` — locked by default. */
|
|
121
|
+
enableRls: string;
|
|
122
|
+
/** `DROP POLICY IF EXISTS` / `CREATE POLICY` statements, in order. */
|
|
123
|
+
policyStatements: string[];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The per-table RLS plan for the *declared* collections, as executable
|
|
127
|
+
* statements — what the managed runtime applies at boot so a freshly
|
|
128
|
+
* provisioned tenant database serves data instead of 401ing every read.
|
|
129
|
+
*
|
|
130
|
+
* Mirrors {@link generatePostgresPoliciesDdl} exactly (same
|
|
131
|
+
* `generatePolicyStatements`, same enable-RLS, same effective rules, same
|
|
132
|
+
* derived junction rules), so boot and `db push` produce identical policies from
|
|
133
|
+
* identical collections.
|
|
134
|
+
*
|
|
135
|
+
* Junction tables are included, and have to be: boot creates them now
|
|
136
|
+
* ({@link planJunctionTables}), and a junction with RLS left off is readable and
|
|
137
|
+
* writable by every signed-in user. A junction whose table is still absent is
|
|
138
|
+
* skipped by the applier, not planned away here.
|
|
139
|
+
*/
|
|
140
|
+
export declare const planCollectionPolicies: (collections: CollectionConfig[]) => CollectionPolicyPlan[];
|
|
8
141
|
export declare const generatePostgresPoliciesDdl: (collections: CollectionConfig[]) => string;
|
|
142
|
+
export {};
|
|
@@ -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>;
|