@intentius/chant-lexicon-sql 0.124.14 → 0.124.15

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 (61) hide show
  1. package/dist/change-set.d.ts +174 -0
  2. package/dist/change-set.d.ts.map +1 -0
  3. package/dist/composites/audit-log-table.d.ts +12 -4
  4. package/dist/composites/audit-log-table.d.ts.map +1 -1
  5. package/dist/composites/cdc-mirror.d.ts +11 -4
  6. package/dist/composites/cdc-mirror.d.ts.map +1 -1
  7. package/dist/composites/events-table.d.ts +10 -3
  8. package/dist/composites/events-table.d.ts.map +1 -1
  9. package/dist/composites/join-table.d.ts +11 -3
  10. package/dist/composites/join-table.d.ts.map +1 -1
  11. package/dist/composites/refreshed-view.d.ts +11 -3
  12. package/dist/composites/refreshed-view.d.ts.map +1 -1
  13. package/dist/composites/replacing-table.d.ts +10 -3
  14. package/dist/composites/replacing-table.d.ts.map +1 -1
  15. package/dist/composites/rollup-view.d.ts +11 -4
  16. package/dist/composites/rollup-view.d.ts.map +1 -1
  17. package/dist/composites/sharded-table.d.ts +11 -4
  18. package/dist/composites/sharded-table.d.ts.map +1 -1
  19. package/dist/composites/soft-delete-table.d.ts +12 -4
  20. package/dist/composites/soft-delete-table.d.ts.map +1 -1
  21. package/dist/composites/tenant-table.d.ts +11 -3
  22. package/dist/composites/tenant-table.d.ts.map +1 -1
  23. package/dist/composites/typed.d.ts +13 -0
  24. package/dist/composites/typed.d.ts.map +1 -0
  25. package/dist/core/typed.d.ts +22 -0
  26. package/dist/core/typed.d.ts.map +1 -1
  27. package/dist/integrity.json +3 -3
  28. package/dist/lint/rules/sqlpg005.d.ts +5 -0
  29. package/dist/lint/rules/sqlpg005.d.ts.map +1 -1
  30. package/dist/manifest.json +1 -1
  31. package/dist/op/activities/clickhouse-rebuild.d.ts +6 -0
  32. package/dist/op/activities/clickhouse-rebuild.d.ts.map +1 -1
  33. package/dist/op/activities/postgres-migration.d.ts +6 -0
  34. package/dist/op/activities/postgres-migration.d.ts.map +1 -1
  35. package/dist/op/activity-contracts.d.ts +30 -0
  36. package/dist/op/activity-contracts.d.ts.map +1 -1
  37. package/dist/postgres/entities.d.ts +12 -3
  38. package/dist/postgres/entities.d.ts.map +1 -1
  39. package/dist/rules/sqlpg005.ts +11 -3
  40. package/package.json +2 -2
  41. package/src/change-set.test.ts +246 -0
  42. package/src/change-set.ts +354 -0
  43. package/src/composites/audit-log-table.ts +11 -4
  44. package/src/composites/cdc-mirror.ts +11 -4
  45. package/src/composites/events-table.ts +8 -3
  46. package/src/composites/join-table.ts +10 -3
  47. package/src/composites/refreshed-view.ts +10 -3
  48. package/src/composites/replacing-table.ts +8 -3
  49. package/src/composites/rollup-view.ts +11 -4
  50. package/src/composites/sharded-table.ts +9 -4
  51. package/src/composites/soft-delete-table.ts +11 -4
  52. package/src/composites/tenant-table.ts +10 -3
  53. package/src/composites/typed.ts +15 -0
  54. package/src/core/typed.ts +40 -0
  55. package/src/lint/rules/sqlpg005.ts +11 -3
  56. package/src/lint/rules/typed-names.test.ts +12 -0
  57. package/src/op/activities/clickhouse-rebuild.ts +11 -1
  58. package/src/op/activities/postgres-migration.ts +11 -1
  59. package/src/op/activity-contracts.ts +6 -2
  60. package/src/postgres/entities.ts +13 -4
  61. package/src/typed-columns.e2e.test.ts +17 -0
@@ -0,0 +1,354 @@
1
+ /**
2
+ * SQL plans as change-set parts (#3767, the document is #3181).
3
+ *
4
+ * The sql lexicon's declarative plans reach the change set through `chant
5
+ * lifecycle plan`. Two kinds of SQL plan do not, and this module is their
6
+ * adapter:
7
+ *
8
+ * - a versioned plan: the migrations a product (SQL Yodeler) would apply to
9
+ * an environment, each a list of steps in the shape `diffStatements`
10
+ * writes (`./core/statements.ts`). {@link sqlVersionedPlanPart}.
11
+ * - the plan step of the lexicon's own migration Ops, `ClickHouseRebuildOp`
12
+ * and `PostgresMigrationOp`. {@link clickhouseRebuildPart} and
13
+ * {@link postgresMigrationPart}.
14
+ *
15
+ * Each returns a part with planner `sql`, so it composes with every other
16
+ * member's in `composeChangeSet`, and a gate that binds the document gets its
17
+ * Cedar `context.plan` (#3182). A part written to a file as JSON reads back
18
+ * through core's `readChangeSetPart` with `planner: "sql"`.
19
+ *
20
+ * ## Entries
21
+ *
22
+ * One entry per object a plan touches, at the object's server name (`db.name`
23
+ * or `schema.name`), however many steps or migrations touch it. The action
24
+ * and disruption come from the change classes the steps carry, as the
25
+ * dialects' `CHANGE_CLASSES` and `PG_CHANGE_CLASSES` price them:
26
+ *
27
+ * - a `drop` class is a `delete` (`destroy`); a `create` class a `create`;
28
+ * - a `rebuild` or `expand` class, or a step a migration Op makes, is a
29
+ * `replace` (`replace`): the object, or a column of it, is built again;
30
+ * - anything else is an `update`, its disruption the worst class's
31
+ * (`metadata` in-place; `validate`, `concurrently` and `rewrite` rolling).
32
+ * A step marked destructive (a column drop) makes it `destroy`. A class
33
+ * neither dialect knows makes it `unknown`.
34
+ *
35
+ * A step may name its `action` itself, which wins over its classes.
36
+ *
37
+ * ## The import graph
38
+ *
39
+ * Like `@intentius/chant/change-set`, this module bundles small for a tool
40
+ * that runs it in CI with no TypeScript toolchain: it imports chant's plan
41
+ * hashing code and types only. `change-set.test.ts` holds that.
42
+ */
43
+
44
+ import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
45
+ import type {
46
+ ChangeSetAction,
47
+ ChangeSetAttribute,
48
+ ChangeSetDisruption,
49
+ ChangeSetEntry,
50
+ ChangeSetHole,
51
+ ChangeSetPart,
52
+ } from "@intentius/chant/change-set";
53
+ import type { MigrationStep, StatementChange } from "./core/statements";
54
+
55
+ /** The lexicon a SQL part's member and entries name. */
56
+ export const SQL_CHANGE_SET_LEXICON = "sql";
57
+
58
+ /**
59
+ * What each change class costs, both dialects together. The same values as
60
+ * `CHANGE_CLASSES.disruption` (ClickHouse) and `PG_CHANGE_CLASSES.disruption`
61
+ * (Postgres), copied so the rule tables stay out of the bundle; the test
62
+ * holds the two together.
63
+ */
64
+ export const SQL_CLASS_DISRUPTION: Readonly<Record<string, ChangeSetDisruption>> = {
65
+ create: "in-place",
66
+ metadata: "in-place",
67
+ validate: "rolling",
68
+ concurrently: "rolling",
69
+ rewrite: "rolling",
70
+ rebuild: "replace",
71
+ expand: "replace",
72
+ drop: "destroy",
73
+ };
74
+
75
+ /**
76
+ * One step of a versioned plan: a `MigrationStep` as `diffStatements` or
77
+ * `chant sql diff --statements --json` writes it, with what a product knows
78
+ * besides. Only `name` and `type` (from `StepObject`) and `kind` are needed.
79
+ */
80
+ export type SqlPlanStep = MigrationStep & {
81
+ /** Already applied by an earlier, partial run: it proposes nothing. */
82
+ done?: boolean;
83
+ /** The action, when the product knows it better than the classes say. */
84
+ action?: ChangeSetAction;
85
+ /** A statement's classified changes, when the product has them; they become the entry's attributes. */
86
+ changes?: StatementChange[];
87
+ };
88
+
89
+ /** A migration of a versioned plan. A step may come wrapped as `{ step, done }`, as SQL Yodeler's plan view holds it. */
90
+ export interface SqlPlanMigration {
91
+ id: string;
92
+ steps: ReadonlyArray<SqlPlanStep | { step: SqlPlanStep; done?: boolean }>;
93
+ }
94
+
95
+ /**
96
+ * A versioned plan, in the shape a product fills: the migrations it would
97
+ * apply to one environment, in order. SQL Yodeler's `VersionedPlan` (`yodel
98
+ * plan <env> --json`) is one as it stands: `pending` for `migrations`, its
99
+ * `refusal` and `digest`.
100
+ */
101
+ export interface SqlVersionedPlan {
102
+ dialect: string;
103
+ /** The environment it plans into: the member's and every entry's `scope`. */
104
+ environment?: string;
105
+ /** The migrations to apply, in order. */
106
+ migrations?: ReadonlyArray<SqlPlanMigration>;
107
+ /** SQL Yodeler's name for `migrations`, read when `migrations` is absent. */
108
+ pending?: ReadonlyArray<SqlPlanMigration>;
109
+ /**
110
+ * The digest the product's own gate binds. When given, it is the member's
111
+ * `planDigest`, so a gate on the member alone and the product's approval
112
+ * bind one value. Absent, the adapter takes `computePlanDigest(
113
+ * "sql-versioned-plan", …)` over the dialect, environment and steps.
114
+ */
115
+ digest?: string;
116
+ /** Why the product would refuse to apply: the member is `failed`. */
117
+ refusal?: string | { message: string };
118
+ /** What the product could not read; the plan says nothing about it. */
119
+ holes?: ChangeSetHole[];
120
+ }
121
+
122
+ export interface SqlVersionedPlanPartInput {
123
+ member: string;
124
+ plan: SqlVersionedPlan;
125
+ /** Default `"sql"`. */
126
+ lexicon?: string;
127
+ }
128
+
129
+ const RANK: Record<ChangeSetDisruption, number> = { "in-place": 0, rolling: 1, unknown: 2, replace: 3, destroy: 4 };
130
+ const worst = (a: ChangeSetDisruption | undefined, b: ChangeSetDisruption): ChangeSetDisruption => (a === undefined || RANK[b] > RANK[a] ? b : a);
131
+ const ACTION_ORDER: ChangeSetAction[] = ["delete", "replace", "create", "update", "forget", "read", "no-op"];
132
+
133
+ /** What the adapter gathers for one object across every step that touches it. */
134
+ interface ObjectChanges {
135
+ address: string;
136
+ type: string;
137
+ classes: string[];
138
+ actions: ChangeSetAction[];
139
+ destructive: boolean;
140
+ /** A step a migration Op makes. */
141
+ ops: string[];
142
+ manual: string[];
143
+ sql: string[];
144
+ fields: Map<string, StatementChange>;
145
+ }
146
+
147
+ const unwrap = (s: SqlPlanStep | { step: SqlPlanStep; done?: boolean }): SqlPlanStep =>
148
+ "step" in s && s.step && typeof s.step === "object" ? { ...s.step, ...(s.done ? { done: true } : {}) } : (s as SqlPlanStep);
149
+
150
+ function gather(steps: Iterable<SqlPlanStep>): ObjectChanges[] {
151
+ const byAddress = new Map<string, ObjectChanges>();
152
+ for (const step of steps) {
153
+ if (step.done) continue;
154
+ let o = byAddress.get(step.name);
155
+ if (!o) {
156
+ o = { address: step.name, type: step.type, classes: [], actions: [], destructive: false, ops: [], manual: [], sql: [], fields: new Map() };
157
+ byAddress.set(step.name, o);
158
+ }
159
+ if (step.action) o.actions.push(step.action);
160
+ const changes: readonly StatementChange[] = step.changes ?? [];
161
+ if (step.kind === "statement") {
162
+ o.classes.push(step.class);
163
+ o.sql.push(step.sql);
164
+ if (step.destructive) o.destructive = true;
165
+ } else if (step.kind === "op") {
166
+ o.ops.push(step.op);
167
+ } else {
168
+ o.manual.push(step.detail);
169
+ }
170
+ for (const c of changes) {
171
+ o.classes.push(c.class);
172
+ if (c.destructive) o.destructive = true;
173
+ const had = o.fields.get(c.field);
174
+ // The first step's before, the last step's after.
175
+ o.fields.set(c.field, had ? { ...c, ...(had.before !== undefined ? { before: had.before } : {}) } : c);
176
+ }
177
+ }
178
+ return [...byAddress.values()];
179
+ }
180
+
181
+ function attributesOf(o: ObjectChanges): ChangeSetAttribute[] {
182
+ const attributes: ChangeSetAttribute[] = [...o.fields.values()]
183
+ .sort((a, b) => (a.field < b.field ? -1 : a.field > b.field ? 1 : 0))
184
+ .map((c) => ({
185
+ path: c.field,
186
+ ...(c.before !== undefined ? { before: c.before } : {}),
187
+ ...(c.after !== undefined ? { after: c.after } : {}),
188
+ ...(SQL_CLASS_DISRUPTION[c.class] === "replace" ? { forcesReplacement: true as const } : {}),
189
+ }));
190
+ if (o.sql.length > 0) attributes.push({ path: "sql", after: o.sql });
191
+ if (o.ops.length > 0) attributes.push({ path: "op", after: [...new Set(o.ops)] });
192
+ if (o.manual.length > 0) attributes.push({ path: "manual", after: o.manual });
193
+ return attributes;
194
+ }
195
+
196
+ /** The action and disruption of one object's changes, as the module's header says. */
197
+ export function sqlChangeAction(o: Pick<ObjectChanges, "classes" | "actions" | "destructive" | "ops">): { action: ChangeSetAction; disruption?: ChangeSetDisruption } {
198
+ let disruption: ChangeSetDisruption | undefined;
199
+ for (const c of o.classes) disruption = worst(disruption, SQL_CLASS_DISRUPTION[c] ?? "unknown");
200
+ if (o.ops.length > 0) disruption = worst(disruption, "replace");
201
+ if (o.destructive) disruption = worst(disruption, "destroy");
202
+ let action: ChangeSetAction;
203
+ if (o.actions.length > 0) action = ACTION_ORDER.find((a) => o.actions.includes(a))!;
204
+ else if (o.classes.includes("drop")) action = "delete";
205
+ else if (o.classes.includes("create")) action = "create";
206
+ else if (o.ops.length > 0 || o.classes.some((c) => SQL_CLASS_DISRUPTION[c] === "replace")) action = "replace";
207
+ else action = "update";
208
+ switch (action) {
209
+ case "create":
210
+ case "read":
211
+ case "no-op":
212
+ case "forget":
213
+ return { action };
214
+ case "delete":
215
+ return { action, disruption: "destroy" };
216
+ case "replace":
217
+ return { action, disruption: worst(disruption, "replace") };
218
+ default:
219
+ return { action, disruption: disruption ?? "unknown" };
220
+ }
221
+ }
222
+
223
+ function entriesOf(member: string, lexicon: string, scope: string | undefined, objects: ObjectChanges[]): ChangeSetEntry[] {
224
+ return objects.map((o) => ({
225
+ member,
226
+ lexicon,
227
+ planner: "sql" as const,
228
+ address: o.address,
229
+ type: o.type,
230
+ ...sqlChangeAction(o),
231
+ ...(scope !== undefined ? { scope } : {}),
232
+ attributes: attributesOf(o),
233
+ }));
234
+ }
235
+
236
+ /**
237
+ * A versioned plan's part ({@link SqlVersionedPlan}): one entry per object
238
+ * its pending steps touch, `scope` the environment. A refusal makes the
239
+ * member `failed` with no plan digest; its entries stay, for a reader.
240
+ */
241
+ export function sqlVersionedPlanPart(input: SqlVersionedPlanPartInput): ChangeSetPart {
242
+ const { plan } = input;
243
+ const lexicon = input.lexicon ?? SQL_CHANGE_SET_LEXICON;
244
+ const migrations = (plan.migrations ?? plan.pending ?? []).map((m) => ({ id: m.id, steps: m.steps.map(unwrap) }));
245
+ const objects = gather(migrations.flatMap((m) => m.steps));
246
+ const refusal = plan.refusal === undefined ? undefined : typeof plan.refusal === "string" ? plan.refusal : plan.refusal.message;
247
+ const planDigest =
248
+ refusal !== undefined
249
+ ? null
250
+ : (plan.digest ??
251
+ computePlanDigest("sql-versioned-plan", {
252
+ dialect: plan.dialect,
253
+ environment: plan.environment ?? null,
254
+ migrations: migrations.map((m) => ({ id: m.id, steps: m.steps.filter((s) => !s.done) })),
255
+ }));
256
+ return {
257
+ member: {
258
+ member: input.member,
259
+ lexicon,
260
+ planner: "sql",
261
+ ...(plan.environment !== undefined ? { scope: plan.environment } : {}),
262
+ status: refusal !== undefined ? "failed" : "planned",
263
+ ...(refusal !== undefined ? { error: refusal } : {}),
264
+ planDigest,
265
+ holes: plan.holes ?? [],
266
+ },
267
+ entries: entriesOf(input.member, lexicon, plan.environment, objects),
268
+ };
269
+ }
270
+
271
+ // ── the migration Ops' plan steps ─────────────────────────────────────────────
272
+
273
+ /**
274
+ * What a migration Op's plan step returns, as far as a part needs it:
275
+ * `clickhouseRebuildPlan`'s `RebuildPlanResult` and `postgresMigrationPlan`'s
276
+ * `MigrationPlanResult` (`./op/activities/`).
277
+ */
278
+ export interface SqlOpPlan {
279
+ state: string;
280
+ /** ClickHouse: the table, `database.name`. */
281
+ table?: string;
282
+ /** Postgres: `schema.table.column`. */
283
+ migration?: string;
284
+ /** The plan's digest; present while there is something to do. */
285
+ planDigest?: string;
286
+ /** Each classified change, as `<rule> <field>: <before> -> <after>`. */
287
+ changes: string[];
288
+ /** The same changes, structured. Each becomes an attribute; without them, only the field is read from `changes`. */
289
+ classified?: StatementChange[];
290
+ }
291
+
292
+ export interface SqlOpPlanPartInput {
293
+ member: string;
294
+ plan: SqlOpPlan;
295
+ /** The environment the Op runs against: the member's and the entry's `scope`. */
296
+ environment?: string;
297
+ /** Default `"sql"`. */
298
+ lexicon?: string;
299
+ }
300
+
301
+ /** A change as a plain string, read back as far as its rule and field go. */
302
+ function changeOf(line: string): StatementChange {
303
+ const m = /^(\S+)\s+([^\s:]+)/.exec(line);
304
+ return { rule: m?.[1] ?? line, class: "unknown", field: m?.[2] ?? line };
305
+ }
306
+
307
+ function opPart(input: SqlOpPlanPartInput, op: "ClickHouseRebuildOp" | "PostgresMigrationOp", active: string, address: string, type: string): ChangeSetPart {
308
+ const { plan } = input;
309
+ const lexicon = input.lexicon ?? SQL_CHANGE_SET_LEXICON;
310
+ const running = plan.state === active;
311
+ const changes = plan.classified ?? plan.changes.map(changeOf);
312
+ const objects = running
313
+ ? gather([{ kind: "op", object: address, type, name: address, op, importPath: "", options: {}, declaration: "", changes, detail: "" }])
314
+ : [];
315
+ // Swapped or done: the Op has nothing left to change in the schema; the digest says which.
316
+ const planDigest = running && plan.planDigest ? plan.planDigest : computePlanDigest("sql-op-plan", { op, address, state: plan.state, planDigest: plan.planDigest ?? null });
317
+ return {
318
+ member: {
319
+ member: input.member,
320
+ lexicon,
321
+ planner: "sql",
322
+ ...(input.environment !== undefined ? { scope: input.environment } : {}),
323
+ status: "planned",
324
+ planDigest,
325
+ holes: [],
326
+ },
327
+ entries: entriesOf(input.member, lexicon, input.environment, objects),
328
+ };
329
+ }
330
+
331
+ /**
332
+ * The part of a `ClickHouseRebuildOp`'s plan step: while the state is
333
+ * `rebuild`, one `replace` of the table, its plan digest the step's (the
334
+ * value before the verification, not the swap gate's). `swapped` and `done`
335
+ * propose nothing.
336
+ */
337
+ export function clickhouseRebuildPart(input: SqlOpPlanPartInput): ChangeSetPart {
338
+ const table = input.plan.table;
339
+ if (!table) throw new Error("clickhouseRebuildPart: the plan names no table");
340
+ return opPart(input, "ClickHouseRebuildOp", "rebuild", table, "ClickHouse::Table");
341
+ }
342
+
343
+ /**
344
+ * The part of a `PostgresMigrationOp`'s plan step: while the state is
345
+ * `migrate`, one `replace` of the table (`schema.table`) whose column it
346
+ * expands and contracts, its plan digest the step's. `switched` and `done`
347
+ * propose nothing.
348
+ */
349
+ export function postgresMigrationPart(input: SqlOpPlanPartInput): ChangeSetPart {
350
+ const migration = input.plan.migration;
351
+ if (!migration) throw new Error("postgresMigrationPart: the plan names no migration");
352
+ const table = migration.split(".").slice(0, -1).join(".") || migration;
353
+ return opPart(input, "PostgresMigrationOp", "migrate", table, "Postgres::Table");
354
+ }
@@ -13,6 +13,8 @@
13
13
 
14
14
  import { Composite } from "@intentius/chant/composite";
15
15
  import { index, table, type PostgresIndex, type PostgresSchema, type PostgresTable } from "../postgres/entities";
16
+ import type { PostgresMemberName } from "../core/typed";
17
+ import type { Instance, TypedComposite } from "./typed";
16
18
 
17
19
  export interface AuditLogTableProps {
18
20
  /** The table's name, unqualified. The default partition is `<name>_default`. */
@@ -25,14 +27,19 @@ export interface AuditLogTableProps {
25
27
  actor?: string;
26
28
  }
27
29
 
28
- export type AuditLogTableMembers = {
29
- table: PostgresTable;
30
- defaultPartition: PostgresTable;
30
+ /** `Table` and `Default` are the table's and its default partition's qualified names, when the call's props give them (chant#3787). */
31
+ export type AuditLogTableMembers<Table extends string = string, Default extends string = string> = {
32
+ table: PostgresTable<Table>;
33
+ defaultPartition: PostgresTable<Default>;
31
34
  actorIndex: PostgresIndex;
32
35
  };
33
36
 
37
+ type AuditLogTableCall = <const N extends string, const S extends PostgresSchema | string = "public">(
38
+ props: AuditLogTableProps & { name: N; schema?: S },
39
+ ) => Instance<AuditLogTableMembers<PostgresMemberName<S, N>, PostgresMemberName<S, N, "_default">>>;
40
+
34
41
  /** An append-only audit log partitioned by month on its timestamp, with a default partition and an index by actor. */
35
- export const AuditLogTable = Composite<AuditLogTableProps, AuditLogTableMembers>((props) => {
42
+ export const AuditLogTable = Composite<AuditLogTableProps, AuditLogTableMembers, TypedComposite<AuditLogTableProps, AuditLogTableMembers, AuditLogTableCall>>((props) => {
36
43
  const schema = props.schema ?? "public";
37
44
  const ts = props.timestamp ?? "occurred_at";
38
45
  const actor = props.actor ?? "actor";
@@ -11,6 +11,8 @@
11
11
 
12
12
  import { Composite } from "@intentius/chant/composite";
13
13
  import { table, view, type ClickHouseTable, type ClickHouseView } from "../clickhouse/entities";
14
+ import type { ClickHouseMemberName } from "../core/typed";
15
+ import type { Instance, TypedComposite } from "./typed";
14
16
 
15
17
  export interface CdcMirrorProps {
16
18
  /** The mirror table's name. The view of live rows is `<name>_current`. */
@@ -27,13 +29,18 @@ export interface CdcMirrorProps {
27
29
  partitionBy?: string;
28
30
  }
29
31
 
30
- export type CdcMirrorMembers = {
31
- table: ClickHouseTable;
32
- current: ClickHouseView;
32
+ /** `Table` and `Current` are the mirror's and its view's names, when the call's props give them (chant#3787). */
33
+ export type CdcMirrorMembers<Table extends string = string, Current extends string = string> = {
34
+ table: ClickHouseTable<Table>;
35
+ current: ClickHouseView<Current>;
33
36
  };
34
37
 
38
+ type CdcMirrorCall = <const N extends string>(
39
+ props: CdcMirrorProps & { name: N },
40
+ ) => Instance<CdcMirrorMembers<ClickHouseMemberName<N>, ClickHouseMemberName<N, "_current">>>;
41
+
35
42
  /** A ReplacingMergeTree mirror of a CDC-fed source table, with a view of the rows not deleted. */
36
- export const CdcMirror = Composite<CdcMirrorProps, CdcMirrorMembers>((props) => {
43
+ export const CdcMirror = Composite<CdcMirrorProps, CdcMirrorMembers, TypedComposite<CdcMirrorProps, CdcMirrorMembers, CdcMirrorCall>>((props) => {
37
44
  const version = props.version ?? "_version";
38
45
  const deleted = props.deleted ?? "_is_deleted";
39
46
  const mirror = table`
@@ -10,6 +10,8 @@
10
10
 
11
11
  import { Composite } from "@intentius/chant/composite";
12
12
  import { table, type ClickHouseTable } from "../clickhouse/entities";
13
+ import type { ClickHouseMemberName } from "../core/typed";
14
+ import type { Instance, TypedComposite } from "./typed";
13
15
 
14
16
  export interface EventsTableProps {
15
17
  /** The table's name, `name` or `database.name`. */
@@ -28,12 +30,15 @@ export interface EventsTableProps {
28
30
  partitionBy?: string;
29
31
  }
30
32
 
31
- export type EventsTableMembers = {
32
- table: ClickHouseTable;
33
+ /** `Table` is the table's name, when the call's props give it (chant#3787). */
34
+ export type EventsTableMembers<Table extends string = string> = {
35
+ table: ClickHouseTable<Table>;
33
36
  };
34
37
 
38
+ type EventsTableCall = <const N extends string>(props: EventsTableProps & { name: N }) => Instance<EventsTableMembers<ClickHouseMemberName<N>>>;
39
+
35
40
  /** An append-only MergeTree events table, partitioned by its timestamp and expired by TTL. */
36
- export const EventsTable = Composite<EventsTableProps, EventsTableMembers>((props) => {
41
+ export const EventsTable = Composite<EventsTableProps, EventsTableMembers, TypedComposite<EventsTableProps, EventsTableMembers, EventsTableCall>>((props) => {
37
42
  const ts = props.timestamp ?? "ts";
38
43
  const events = table`
39
44
  CREATE TABLE ${props.name} (
@@ -10,6 +10,8 @@
10
10
 
11
11
  import { Composite } from "@intentius/chant/composite";
12
12
  import { index, table, type PostgresIndex, type PostgresSchema, type PostgresTable } from "../postgres/entities";
13
+ import type { PostgresMemberName } from "../core/typed";
14
+ import type { Instance, TypedComposite } from "./typed";
13
15
 
14
16
  export interface JoinTableProps {
15
17
  /** The table's name, unqualified. The reverse index is `<name>_reverse_idx`. */
@@ -36,13 +38,18 @@ export interface JoinTableProps {
36
38
  onDelete?: string;
37
39
  }
38
40
 
39
- export type JoinTableMembers = {
40
- table: PostgresTable;
41
+ /** `Table` is the join table's qualified name, when the call's props give it (chant#3787). */
42
+ export type JoinTableMembers<Table extends string = string> = {
43
+ table: PostgresTable<Table>;
41
44
  reverseIndex: PostgresIndex;
42
45
  };
43
46
 
47
+ type JoinTableCall = <const N extends string, const S extends PostgresSchema | string = "public">(
48
+ props: JoinTableProps & { name: N; schema?: S },
49
+ ) => Instance<JoinTableMembers<PostgresMemberName<S, N>>>;
50
+
44
51
  /** The table of a many-to-many relation: two foreign keys, a composite primary key and the reverse index. */
45
- export const JoinTable = Composite<JoinTableProps, JoinTableMembers>((props) => {
52
+ export const JoinTable = Composite<JoinTableProps, JoinTableMembers, TypedComposite<JoinTableProps, JoinTableMembers, JoinTableCall>>((props) => {
46
53
  const schema = props.schema ?? "public";
47
54
  const links = table`
48
55
  CREATE TABLE ${schema}.${props.name} (
@@ -11,6 +11,8 @@
11
11
 
12
12
  import { Composite } from "@intentius/chant/composite";
13
13
  import { index, view, type PostgresIndex, type PostgresRelation, type PostgresSchema, type PostgresView } from "../postgres/entities";
14
+ import type { PostgresMemberName } from "../core/typed";
15
+ import type { Instance, TypedComposite } from "./typed";
14
16
 
15
17
  export interface RefreshedViewProps {
16
18
  /** The view's name, unqualified. The index is `<name>_key`. */
@@ -27,13 +29,18 @@ export interface RefreshedViewProps {
27
29
  uniqueOn?: string;
28
30
  }
29
31
 
30
- export type RefreshedViewMembers = {
31
- view: PostgresView;
32
+ /** `View` is the materialized view's qualified name, when the call's props give it (chant#3787). */
33
+ export type RefreshedViewMembers<View extends string = string> = {
34
+ view: PostgresView<View>;
32
35
  uniqueIndex: PostgresIndex;
33
36
  };
34
37
 
38
+ type RefreshedViewCall = <const N extends string, const S extends PostgresSchema | string = "public">(
39
+ props: RefreshedViewProps & { name: N; schema?: S },
40
+ ) => Instance<RefreshedViewMembers<PostgresMemberName<S, N>>>;
41
+
35
42
  /** A materialized view over a source, with the unique index that allows REFRESH ... CONCURRENTLY. */
36
- export const RefreshedView = Composite<RefreshedViewProps, RefreshedViewMembers>((props) => {
43
+ export const RefreshedView = Composite<RefreshedViewProps, RefreshedViewMembers, TypedComposite<RefreshedViewProps, RefreshedViewMembers, RefreshedViewCall>>((props) => {
37
44
  const schema = props.schema ?? "public";
38
45
  const summary = view`
39
46
  CREATE MATERIALIZED VIEW ${schema}.${props.name} AS
@@ -14,6 +14,8 @@
14
14
 
15
15
  import { Composite } from "@intentius/chant/composite";
16
16
  import { table, type ClickHouseTable } from "../clickhouse/entities";
17
+ import type { ClickHouseMemberName } from "../core/typed";
18
+ import type { Instance, TypedComposite } from "./typed";
17
19
 
18
20
  export interface ReplacingTableProps {
19
21
  /** The table's name, `name` or `database.name`. */
@@ -30,12 +32,15 @@ export interface ReplacingTableProps {
30
32
  partitionBy?: string;
31
33
  }
32
34
 
33
- export type ReplacingTableMembers = {
34
- table: ClickHouseTable;
35
+ /** `Table` is the table's name, when the call's props give it (chant#3787). */
36
+ export type ReplacingTableMembers<Table extends string = string> = {
37
+ table: ClickHouseTable<Table>;
35
38
  };
36
39
 
40
+ type ReplacingTableCall = <const N extends string>(props: ReplacingTableProps & { name: N }) => Instance<ReplacingTableMembers<ClickHouseMemberName<N>>>;
41
+
37
42
  /** A ReplacingMergeTree table that keeps the highest version of each row, with its version column. */
38
- export const ReplacingTable = Composite<ReplacingTableProps, ReplacingTableMembers>((props) => {
43
+ export const ReplacingTable = Composite<ReplacingTableProps, ReplacingTableMembers, TypedComposite<ReplacingTableProps, ReplacingTableMembers, ReplacingTableCall>>((props) => {
39
44
  const version = props.version ?? "version";
40
45
  const replacing = table`
41
46
  CREATE TABLE ${props.name} (
@@ -14,6 +14,8 @@
14
14
 
15
15
  import { Composite } from "@intentius/chant/composite";
16
16
  import { table, view, type ClickHouseRelation, type ClickHouseTable, type ClickHouseView } from "../clickhouse/entities";
17
+ import type { ClickHouseMemberName } from "../core/typed";
18
+ import type { Instance, TypedComposite } from "./typed";
17
19
 
18
20
  export interface RollupViewProps {
19
21
  /** The target table's name. The view is `<name>_mv`. */
@@ -34,13 +36,18 @@ export interface RollupViewProps {
34
36
  partitionBy?: string;
35
37
  }
36
38
 
37
- export type RollupViewMembers = {
38
- table: ClickHouseTable;
39
- view: ClickHouseView;
39
+ /** `Table` and `View` are the target table's and the view's names, when the call's props give them (chant#3787). */
40
+ export type RollupViewMembers<Table extends string = string, View extends string = string> = {
41
+ table: ClickHouseTable<Table>;
42
+ view: ClickHouseView<View>;
40
43
  };
41
44
 
45
+ type RollupViewCall = <const N extends string>(
46
+ props: RollupViewProps & { name: N },
47
+ ) => Instance<RollupViewMembers<ClickHouseMemberName<N>, ClickHouseMemberName<N, "_mv">>>;
48
+
42
49
  /** A materialized view that rolls a source table up into a target table it declares beside it. */
43
- export const RollupView = Composite<RollupViewProps, RollupViewMembers>((props) => {
50
+ export const RollupView = Composite<RollupViewProps, RollupViewMembers, TypedComposite<RollupViewProps, RollupViewMembers, RollupViewCall>>((props) => {
44
51
  const target = table`
45
52
  CREATE TABLE ${props.name} (${props.columns})
46
53
  ENGINE = ${props.engine ?? "SummingMergeTree"}
@@ -14,6 +14,8 @@
14
14
 
15
15
  import { Composite } from "@intentius/chant/composite";
16
16
  import { table, type ClickHouseTable } from "../clickhouse/entities";
17
+ import type { ClickHouseMemberName } from "../core/typed";
18
+ import type { Instance, TypedComposite } from "./typed";
17
19
 
18
20
  export interface ShardedTableProps {
19
21
  /** The Distributed table's name, unqualified. The local table is `<name>_local`. */
@@ -30,13 +32,16 @@ export interface ShardedTableProps {
30
32
  partitionBy?: string;
31
33
  }
32
34
 
33
- export type ShardedTableMembers = {
34
- local: ClickHouseTable;
35
- distributed: ClickHouseTable;
35
+ /** `Local` and `Distributed` are the two tables' names, when the call's props give them (chant#3787). */
36
+ export type ShardedTableMembers<Local extends string = string, Distributed extends string = string> = {
37
+ local: ClickHouseTable<Local>;
38
+ distributed: ClickHouseTable<Distributed>;
36
39
  };
37
40
 
41
+ type ShardedTableCall = <const N extends string>(props: ShardedTableProps & { name: N }) => Instance<ShardedTableMembers<ClickHouseMemberName<N, "_local">, ClickHouseMemberName<N>>>;
42
+
38
43
  /** A ReplicatedMergeTree table on every node of a cluster and the Distributed table over it. */
39
- export const ShardedTable = Composite<ShardedTableProps, ShardedTableMembers>((props) => {
44
+ export const ShardedTable = Composite<ShardedTableProps, ShardedTableMembers, TypedComposite<ShardedTableProps, ShardedTableMembers, ShardedTableCall>>((props) => {
40
45
  const local = table`
41
46
  CREATE TABLE ${`${props.name}_local`} ON CLUSTER ${props.cluster} (${props.columns})
42
47
  ENGINE = ReplicatedMergeTree
@@ -14,6 +14,8 @@
14
14
 
15
15
  import { Composite } from "@intentius/chant/composite";
16
16
  import { index, literal, table, view, type PostgresIndex, type PostgresSchema, type PostgresTable, type PostgresView } from "../postgres/entities";
17
+ import type { PostgresMemberName } from "../core/typed";
18
+ import type { Instance, TypedComposite } from "./typed";
17
19
 
18
20
  export interface SoftDeleteTableProps {
19
21
  /** The table's name, unqualified. The view is `<name>_live`. */
@@ -34,14 +36,19 @@ export interface SoftDeleteTableProps {
34
36
  deletedAt?: string;
35
37
  }
36
38
 
37
- export type SoftDeleteTableMembers = {
38
- table: PostgresTable;
39
- live: PostgresView;
39
+ /** `Table` and `Live` are the table's and the view's qualified names, when the call's props give them (chant#3787). */
40
+ export type SoftDeleteTableMembers<Table extends string = string, Live extends string = string> = {
41
+ table: PostgresTable<Table>;
42
+ live: PostgresView<Live>;
40
43
  liveIndex: PostgresIndex;
41
44
  };
42
45
 
46
+ type SoftDeleteTableCall = <const N extends string, const S extends PostgresSchema | string = "public">(
47
+ props: SoftDeleteTableProps & { name: N; schema?: S },
48
+ ) => Instance<SoftDeleteTableMembers<PostgresMemberName<S, N>, PostgresMemberName<S, N, "_live">>>;
49
+
43
50
  /** A table with created and updated timestamps and a soft-delete column, a view of its live rows and an index over them. */
44
- export const SoftDeleteTable = Composite<SoftDeleteTableProps, SoftDeleteTableMembers>((props) => {
51
+ export const SoftDeleteTable = Composite<SoftDeleteTableProps, SoftDeleteTableMembers, TypedComposite<SoftDeleteTableProps, SoftDeleteTableMembers, SoftDeleteTableCall>>((props) => {
45
52
  const schema = props.schema ?? "public";
46
53
  const deleted = props.deletedAt ?? "deleted_at";
47
54
  const rows = table`