okmodel 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +263 -23
  2. package/dist/contracts/catalog/document.d.ts +26 -2
  3. package/dist/contracts/driver.d.ts +7 -0
  4. package/dist/dialects/pg/ext/citext.js +1 -1
  5. package/dist/dialects/pg/ext/index.js +1 -1
  6. package/dist/dialects/pg/ext/pg-trgm.js +1 -1
  7. package/dist/dialects/pg/index.js +3 -3
  8. package/dist/dialects/pg/introspect.d.ts +6 -0
  9. package/dist/dialects/pg/role/sql.d.ts +21 -4
  10. package/dist/dialects/pg/schema.d.ts +15 -6
  11. package/dist/dialects/pg/table.d.ts +17 -1
  12. package/dist/dialects/pg/view/index.d.ts +20 -6
  13. package/dist/dialects/pg/view/index.js +1 -1
  14. package/dist/dialects/pg/view/scratch.d.ts +2 -1
  15. package/dist/runtime/catalog-cache.d.ts +17 -0
  16. package/dist/runtime/compat-words.d.ts +23 -0
  17. package/dist/runtime/drift-detail.d.ts +20 -4
  18. package/dist/runtime/drift.d.ts +4 -2
  19. package/dist/runtime/pg/bun.d.ts +12 -7
  20. package/dist/runtime/pg/bun.js +5 -2
  21. package/dist/runtime/pg/pg.d.ts +12 -6
  22. package/dist/runtime/pg/pg.js +5 -2
  23. package/dist/runtime/pg/pglite.d.ts +11 -3
  24. package/dist/runtime/pg/pglite.js +5 -2
  25. package/dist/runtime/pg/postgresjs.d.ts +12 -6
  26. package/dist/runtime/pg/postgresjs.js +5 -2
  27. package/dist/runtime/plan.d.ts +3 -1
  28. package/dist/runtime/tenancy/index.js +1 -1
  29. package/dist/runtime/topology.d.ts +191 -0
  30. package/dist/runtime/traits/index.js +7 -6
  31. package/dist/runtime/types.d.ts +59 -2
  32. package/dist/runtime/validate/index.js +2 -2
  33. package/dist/shared/{x6wysh83.js → 3p4b1fg0.js} +3 -5
  34. package/dist/shared/{ty3v3hfk.js → 47xzb9fc.js} +2 -2
  35. package/dist/shared/{fs0ex00p.js → 4ef7mmqf.js} +2 -2
  36. package/dist/shared/{qybh48nd.js → 5fqx9yh4.js} +6 -2
  37. package/dist/shared/5mtf6cj3.js +94 -0
  38. package/dist/shared/{999ade2q.js → 5vdm4sgp.js} +1 -1
  39. package/dist/shared/{bqha5cg6.js → 6p5k20sm.js} +2 -2
  40. package/dist/shared/{t616ynfn.js → 8bwbwy8h.js} +71 -13
  41. package/dist/shared/{th14ygv2.js → 8ktwd3rh.js} +29 -1
  42. package/dist/shared/{wekze34d.js → 8xsbagad.js} +74 -2
  43. package/dist/shared/{bryqk9k9.js → 922qbgnw.js} +3 -3
  44. package/dist/shared/{meck53jv.js → 990a291j.js} +6 -5
  45. package/dist/shared/{jns2wx4t.js → 9tgmr1gh.js} +1 -1
  46. package/dist/shared/9yn6a2vd.js +11 -0
  47. package/dist/shared/{2ssqrv66.js → ayz64vzj.js} +1037 -181
  48. package/dist/shared/{k34e0xf2.js → bt9sfq0z.js} +16 -14
  49. package/dist/shared/{scwxrpx2.js → bzsjtd7c.js} +84 -15
  50. package/dist/shared/{m9tv7xn7.js → ckjbyrt9.js} +4 -4
  51. package/dist/shared/{ydaz1sv3.js → dxjp14rk.js} +10 -2
  52. package/dist/shared/{49ypnfwt.js → fancm5pt.js} +24 -229
  53. package/dist/shared/{6bazvqwm.js → fb00y8fp.js} +2 -2
  54. package/dist/shared/{1tfdfvhh.js → fyqkk1m0.js} +6 -4
  55. package/dist/shared/g516zdsb.js +905 -0
  56. package/dist/shared/{jkc6ws1p.js → g7ez901n.js} +83 -2
  57. package/dist/shared/{kgnc8grt.js → gdh9mrtp.js} +1 -1
  58. package/dist/shared/{p24x2334.js → h4nqvjkp.js} +5 -3
  59. package/dist/shared/{edwqqryd.js → h8ne16fq.js} +1 -1
  60. package/dist/shared/htnaj77v.js +17 -0
  61. package/dist/shared/jdgqqx31.js +72 -0
  62. package/dist/shared/{6w5j844a.js → jsd07prz.js} +2 -2
  63. package/dist/shared/{m78s7898.js → k7cc9nzk.js} +24 -7
  64. package/dist/shared/kdkxh3kb.js +758 -0
  65. package/dist/shared/nkc9sfqb.js +1161 -0
  66. package/dist/shared/{xva8v7qk.js → p7pfx624.js} +5 -5
  67. package/dist/shared/{xrfnq5k8.js → qgvt8nx1.js} +11 -2
  68. package/dist/shared/{ne02ng5c.js → qy9rdq0g.js} +9 -5
  69. package/dist/shared/{3ekj9x92.js → r6pj4fq2.js} +5 -3
  70. package/dist/shared/r73pt594.js +426 -0
  71. package/dist/shared/rasvvzgz.js +21 -0
  72. package/dist/shared/{01yr20s5.js → sf8m6tfy.js} +2 -2
  73. package/dist/shared/{hcmdtwgh.js → szm0cksa.js} +4 -1
  74. package/dist/shared/{axhk06q1.js → tq9afp8d.js} +1 -1
  75. package/dist/shared/{xw1d69vd.js → wqem4jed.js} +6 -6
  76. package/dist/shared/wxn4b7pp.js +252 -0
  77. package/dist/shared/xdpj5kkm.js +2203 -0
  78. package/dist/shared/xhh1srw3.js +589 -0
  79. package/dist/shared/{mt5wswjz.js → xsq7v6wp.js} +2 -2
  80. package/dist/shared/{7fvtrkwr.js → ycatkxmq.js} +1 -1
  81. package/dist/shared/ykv7wh5b.js +14 -0
  82. package/dist/shared/{fptpmvxp.js → z2rnngj4.js} +26 -21
  83. package/dist/tooling/cli.js +46 -17
  84. package/dist/tooling/migrate/apply.d.ts +61 -5
  85. package/dist/tooling/migrate/backfill.d.ts +136 -0
  86. package/dist/tooling/migrate/check.d.ts +65 -0
  87. package/dist/tooling/migrate/classify.d.ts +108 -0
  88. package/dist/tooling/migrate/commands.d.ts +2 -2
  89. package/dist/tooling/migrate/config.d.ts +22 -0
  90. package/dist/tooling/migrate/doctor.d.ts +3 -0
  91. package/dist/tooling/migrate/drift.d.ts +18 -2
  92. package/dist/tooling/migrate/estimate.d.ts +50 -0
  93. package/dist/tooling/migrate/extensions.d.ts +2 -0
  94. package/dist/tooling/migrate/files.d.ts +19 -0
  95. package/dist/tooling/migrate/index.d.ts +3 -2
  96. package/dist/tooling/migrate/index.js +69 -1
  97. package/dist/tooling/migrate/lint-rules.d.ts +51 -0
  98. package/dist/tooling/migrate/lint.d.ts +71 -0
  99. package/dist/tooling/migrate/managed.d.ts +1 -1
  100. package/dist/tooling/migrate/plan.d.ts +64 -9
  101. package/dist/tooling/migrate/project.d.ts +42 -7
  102. package/dist/tooling/migrate/provision.d.ts +18 -0
  103. package/dist/tooling/migrate/reference.d.ts +40 -0
  104. package/dist/tooling/migrate/runner.d.ts +108 -0
  105. package/dist/tooling/migrate/safe.d.ts +77 -0
  106. package/dist/tooling/migrate/seed.d.ts +20 -0
  107. package/dist/tooling/migrate/snapshot.d.ts +78 -0
  108. package/dist/tooling/migrate/status.d.ts +25 -6
  109. package/dist/tooling/testing/factories.d.ts +117 -0
  110. package/dist/tooling/testing/facts.d.ts +75 -0
  111. package/dist/tooling/testing/index.d.ts +89 -0
  112. package/dist/tooling/testing/index.js +8 -0
  113. package/dist/tooling/testing/isolation.d.ts +30 -0
  114. package/dist/tooling/testing/random.d.ts +40 -0
  115. package/dist/tooling/testing/record.d.ts +38 -0
  116. package/package.json +6 -2
  117. package/dist/shared/5nmdenb6.js +0 -1229
  118. package/dist/shared/dbjej1tw.js +0 -456
  119. package/dist/shared/g4f5kcx9.js +0 -37
  120. package/dist/shared/qbxzvwwc.js +0 -88
  121. package/dist/shared/zwmqxb0g.js +0 -79
  122. /package/dist/shared/{75xkjv0t.js → fa9vt245.js} +0 -0
  123. /package/dist/shared/{0jc3ffzf.js → zvbjc949.js} +0 -0
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Safe DDL for a table that already exists (D193).
3
+ *
4
+ * A new table keeps the plain statements from `ddl.ts`. These shapes are only
5
+ * for a table in the before-catalog: concurrent indexes, `NOT VALID` plus
6
+ * `VALIDATE`, `SET NOT NULL` through a validated check, a volatile default
7
+ * split from column creation, and a unique or primary key built from a
8
+ * concurrent unique index.
9
+ *
10
+ * The volatile-default fill is a backfill: one idempotent `UPDATE` per batch,
11
+ * limited to rows where the new column is still null (D195).
12
+ */
13
+ import type { Catalog, CatalogObject, ColumnObject } from "../../contracts/catalog/types.js";
14
+ import { type BackfillSpec } from "./backfill.js";
15
+ import type { StepKind } from "./classify.js";
16
+ /** One statement the planner emits instead of the plain form. */
17
+ export type SafeStep = {
18
+ readonly sql: string;
19
+ readonly kind: StepKind;
20
+ readonly action: "ddl" | "backfill";
21
+ readonly lock: string;
22
+ readonly transactional: boolean;
23
+ /** Set on the volatile-default fill. The statement is one batch. */
24
+ readonly backfill?: BackfillSpec;
25
+ };
26
+ /**
27
+ * Safe steps for one object on a table that already exists.
28
+ *
29
+ * Returns `undefined` when the plain statement is already safe: a stable or
30
+ * constant column default, or an object this rewrite does not cover.
31
+ *
32
+ * @param object - Object the plan is creating
33
+ * @param schema - Concrete schema name
34
+ * @param catalogs - Before and after catalogs, for volatility and name clashes
35
+ * @param batchSize - Rows per batch written on a volatile-default fill
36
+ * @returns The replacement steps, or `undefined` to keep the plain statement
37
+ */
38
+ export declare function stepsForExistingTable(object: CatalogObject, schema: string, catalogs: readonly Catalog[], batchSize: number): readonly SafeStep[] | undefined;
39
+ /**
40
+ * `SET NOT NULL` through a validated check (Postgres 12+, always on the floor of 15).
41
+ *
42
+ * Postgres skips the table scan when a validated `CHECK (col IS NOT NULL)`
43
+ * already exists. The temporary check is dropped after `SET NOT NULL`.
44
+ *
45
+ * @param column - Column that must become `NOT NULL`
46
+ * @param schema - Concrete schema name
47
+ * @param setKind - `set-not-null` when the column already existed; `add-column` when this finishes a new column and the plan stays expand
48
+ * @param catalogs - Catalogs whose constraint names the temporary check must not reuse
49
+ * @returns Add `NOT VALID`, validate, set not null, drop the check
50
+ */
51
+ export declare function notNullSteps(column: ColumnObject, schema: string, setKind: "set-not-null" | "add-column", catalogs: readonly Catalog[]): readonly SafeStep[];
52
+ /**
53
+ * Inserts `CONCURRENTLY` into a `CREATE INDEX` statement.
54
+ *
55
+ * @param sql - Plain `create index` or `create unique index` from `ddl.ts`
56
+ * @returns The same statement, concurrent
57
+ */
58
+ export declare function concurrentIndexSql(sql: string): string;
59
+ /**
60
+ * Inserts `CONCURRENTLY` into a `DROP INDEX` statement.
61
+ *
62
+ * @param sql - Plain `drop index` from `ddl.ts`
63
+ * @returns The same statement, concurrent
64
+ */
65
+ export declare function concurrentDropIndexSql(sql: string): string;
66
+ /**
67
+ * Reports whether a default expression is volatile.
68
+ *
69
+ * A constant has no call. `now()` and the other stable builtins stay plain.
70
+ * `gen_random_uuid()`, `uuidv7()`, and any other call the catalog marks
71
+ * volatile are split. A call the catalog does not know is left plain.
72
+ *
73
+ * @param expression - Column `defaultExpression`, when the column has one
74
+ * @param catalogs - Catalogs that may hold the called function
75
+ * @returns `true` when adding the column with this default would rewrite existing rows
76
+ */
77
+ export declare function volatileDefault(expression: string | undefined, catalogs: readonly Catalog[]): boolean;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `okm seed <file>` (spec §20, D199).
3
+ *
4
+ * The file's default export receives the testing harness for the selected
5
+ * target. Protection is `assertTargetPolicy`, the same gate as every other
6
+ * command. The target is already migrated: this command does not apply DDL.
7
+ */
8
+ import { type InvokeFlags } from "./policy.js";
9
+ /**
10
+ * Runs a seed file against the selected target.
11
+ *
12
+ * Policy and target selection run before the pool opens, so a protected
13
+ * target is refused without a connection.
14
+ *
15
+ * @param cwd - Project directory
16
+ * @param file - Seed module, relative to `cwd`
17
+ * @param flags - `--target`, `--allow-protected`, and `--allow-pooler`
18
+ * @returns Text for stdout
19
+ */
20
+ export declare function seedProject(cwd: string, file: string, flags: InvokeFlags): Promise<string>;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Head snapshot install and the comparison with a replayed history (D198).
3
+ *
4
+ * Provision plans the head catalog from an empty catalog and runs that SQL.
5
+ * It does not replay migration files. OKM1521 is the difference between that
6
+ * result and a full replay, after {@link canonicalTypeName} (D191).
7
+ */
8
+ import type { Catalog } from "../../contracts/catalog/types.js";
9
+ import type { DriverConnection } from "../../contracts/driver.js";
10
+ import type { ReferenceTable } from "./reference.js";
11
+ /** History id prefix for a target installed from the head snapshot. */
12
+ export declare const PROVISION_PREFIX = "provisioned@";
13
+ /**
14
+ * History id written when a target is provisioned at `migrationId`.
15
+ *
16
+ * @param migrationId - Head migration id
17
+ * @returns `provisioned@` plus that id
18
+ */
19
+ export declare function provisionMark(migrationId: string): string;
20
+ /**
21
+ * Migration id stored inside a provision mark.
22
+ *
23
+ * @param migrationId - A history migration id
24
+ * @returns The id after the prefix, or `undefined` when it is not a mark
25
+ */
26
+ export declare function provisionedMigrationId(migrationId: string): string | undefined;
27
+ /**
28
+ * Marks every step up through a provisioned migration as done.
29
+ *
30
+ * A later file stays pending. A mark whose id is not in `migrations` is left
31
+ * for the caller to treat as history this project does not contain.
32
+ *
33
+ * @param migrations - Files in apply order
34
+ * @param done - Keys `${id}:${step}` already in history
35
+ * @param historyIds - `migration_id` values in history
36
+ * @returns A set the applier can skip
37
+ */
38
+ export declare function expandProvisioned(migrations: readonly {
39
+ readonly id: string;
40
+ readonly steps: readonly unknown[];
41
+ }[], done: ReadonlySet<string>, historyIds: readonly string[]): Set<string>;
42
+ /**
43
+ * Reports whether a history id belongs to this project's files.
44
+ *
45
+ * A `provisioned@` mark belongs when the id after the prefix is a file.
46
+ *
47
+ * @param migrationId - History migration id
48
+ * @param known - File ids
49
+ * @returns `true` when status should not call the row ahead
50
+ */
51
+ export declare function provisionHistoryIsKnown(migrationId: string, known: ReadonlySet<string>): boolean;
52
+ /**
53
+ * Installs `head` into an empty schema and inserts reference rows.
54
+ *
55
+ * The plan is from an empty catalog, so no expand, backfill, or contract
56
+ * file is replayed. An existing role is left in place. The caller records
57
+ * history.
58
+ *
59
+ * @param connection - Reserved connection, after the advisory lock
60
+ * @param head - Head snapshot
61
+ * @param schema - Schema that receives the objects
62
+ * @param reference - Rows to insert if missing
63
+ * @param migrationRole - Role assumed after this run creates it
64
+ */
65
+ export declare function installSnapshot(connection: DriverConnection, head: Catalog, schema: string, reference: readonly ReferenceTable[], session: {
66
+ setRole?: string;
67
+ }, migrationRole?: string): Promise<void>;
68
+ /**
69
+ * Names the first object that differs between two introspected catalogs.
70
+ *
71
+ * Type names are compared with {@link canonicalTypeName}. Managed history
72
+ * tables are omitted. `undefined` means the catalogs match.
73
+ *
74
+ * @param left - One introspection
75
+ * @param right - The other introspection
76
+ * @returns The object, and the statement that would change it, when they differ
77
+ */
78
+ export declare function snapshotDifference(left: Catalog, right: Catalog): string | undefined;
@@ -2,8 +2,10 @@
2
2
  * `okm migrate status`.
3
3
  *
4
4
  * One row per target: version, catalog hash, state, and a separate protected
5
- * column. The state is current, behind by expand, behind by contract, ahead,
6
- * or failed at a step.
5
+ * column. The state is current, behind by expand, behind by contract, ahead
6
+ * by expand, ahead by contract, or failed at a step with a resume hint. An
7
+ * unfinished backfill is a later section: migration, step, rows so far, last
8
+ * key, and state.
7
9
  */
8
10
  import type { MigrationClass } from "./plan.js";
9
11
  import type { StoredMigration } from "./files.js";
@@ -15,6 +17,18 @@ export type TargetStatus = {
15
17
  readonly catalogHash: string;
16
18
  readonly state: string;
17
19
  readonly protected: boolean;
20
+ /** Unfinished `okm_backfill` rows for this target. A finished step is absent. */
21
+ readonly backfills: readonly BackfillProgress[];
22
+ };
23
+ /** One backfill that has committed at least one batch and is not done. */
24
+ export type BackfillProgress = {
25
+ readonly migrationId: string;
26
+ readonly stepIndex: number;
27
+ /** Rows the committed batches changed. */
28
+ readonly rows: string;
29
+ /** Last committed boundary, or `-` when the first batch is still open. */
30
+ readonly lastKey: string;
31
+ readonly state: string;
18
32
  };
19
33
  /** A checkpoint row the state function reads. */
20
34
  export type StatusHistory = {
@@ -25,9 +39,11 @@ export type StatusHistory = {
25
39
  /**
26
40
  * Decides the status state from files and history.
27
41
  *
28
- * A partial migration is `failed at step`. History the files do not contain
29
- * is `ahead`. Pending files are behind by expand, or by contract when any
30
- * pending step is not expand.
42
+ * A partial migration is `failed at step` plus the resume hint. History the
43
+ * files do not contain is ahead by expand, or ahead by contract when a stored
44
+ * class is not expand. Pending files are behind by expand, or by contract
45
+ * when any pending step is not expand. The class is the one stored on the
46
+ * file or in `okm_history`.
31
47
  *
32
48
  * @param migrations - Files in apply order
33
49
  * @param history - Checkpoint rows
@@ -68,7 +84,10 @@ export declare function statusProject(cwd: string, flags: InvokeFlags): Promise<
68
84
  /**
69
85
  * Prints status rows.
70
86
  *
87
+ * The target columns stay `target`, `version`, `catalog`, `state`, and
88
+ * `protected`. Unfinished backfills follow as their own section.
89
+ *
71
90
  * @param rows - One per target
72
- * @returns Header plus one line per target
91
+ * @returns Header plus one line per target, then unfinished backfills
73
92
  */
74
93
  export declare function formatStatus(rows: readonly TargetStatus[]): string;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Factories: `create`, `createMany`, `with({ child: n })`, and `x.ref`.
3
+ *
4
+ * A required column the definition leaves out is filled from its type (D199).
5
+ * `x.ref` reuses the latest row of that table in the same tenant, or creates one.
6
+ */
7
+ import type { Connected } from "../../runtime/types.js";
8
+ import { type TableFacts, type TestingSchema } from "./facts.js";
9
+ import type { Rng } from "./random.js";
10
+ declare const REF: unique symbol;
11
+ /** A `x.ref("table")` marker. Resolved when the row is inserted. */
12
+ type FactoryRef = {
13
+ readonly [REF]: string;
14
+ };
15
+ /** Values a factory definition may return. */
16
+ export type FactoryContext = {
17
+ /** `userN@example.com`. N increases for each call. */
18
+ email(): string;
19
+ /** A first and last name from a fixed list. */
20
+ name(): string;
21
+ /** `count` words from a fixed list. */
22
+ words(count: number): string;
23
+ /** An integer in `[0, bound)`. `bound` defaults to 10_000. */
24
+ int(bound?: number): number;
25
+ /** A number in `[0, 1)`. */
26
+ float(): number;
27
+ /** A boolean. */
28
+ boolean(): boolean;
29
+ /** An ISO date `YYYY-MM-DD` in January 2020. */
30
+ date(): string;
31
+ /** A UUID from the seed. */
32
+ uuid(): string;
33
+ /** One entry of `list`. */
34
+ pick<T>(list: readonly T[]): T;
35
+ /**
36
+ * The primary key of `table`.
37
+ *
38
+ * The first call creates the row. A later call in the same tenant reuses it.
39
+ * A `create` of that table replaces the reused row.
40
+ */
41
+ ref(table: string): FactoryRef;
42
+ };
43
+ /** One table's factory. */
44
+ export type TableFactory = {
45
+ /**
46
+ * Inserts one row.
47
+ *
48
+ * @param overrides - Column values. A tenant key here is the scope, not a column.
49
+ * @returns The inserted row
50
+ */
51
+ create(overrides?: Readonly<Record<string, unknown>>): Promise<Record<string, unknown>>;
52
+ /**
53
+ * Inserts `count` rows. Each row draws new generated values.
54
+ *
55
+ * @param count - How many rows
56
+ * @param overrides - Column values applied to every row
57
+ * @returns The inserted rows
58
+ */
59
+ createMany(count: number, overrides?: Readonly<Record<string, unknown>>): Promise<readonly Record<string, unknown>[]>;
60
+ /**
61
+ * Creates `count` rows of `child` after the parent, linked by the foreign key.
62
+ *
63
+ * @param children - Child table name to how many rows
64
+ * @returns A factory that still inserts this table
65
+ */
66
+ with(children: Readonly<Record<string, number>>): TableFactory;
67
+ };
68
+ /**
69
+ * Remembers `host` for the harness object `okm seed` prints from.
70
+ *
71
+ * @param harness - The object returned by `testing()`
72
+ * @param host - Its factory state
73
+ */
74
+ export declare function attachHost(harness: object, host: FactoryHost): void;
75
+ /**
76
+ * Rows inserted through factories, summed by table name.
77
+ *
78
+ * @param harness - The object returned by `testing()`
79
+ * @returns Counts in table-name order
80
+ */
81
+ export declare function createdOf(harness: object): readonly {
82
+ readonly table: string;
83
+ readonly count: number;
84
+ }[];
85
+ /** Shared state for one harness. */
86
+ export type FactoryHost = {
87
+ readonly schema: TestingSchema;
88
+ readonly db: Connected<TestingSchema>;
89
+ readonly facts: ReadonlyMap<string, TableFacts>;
90
+ readonly rng: Rng;
91
+ /** Latest row of a table in one tenant. Isolation uses its own map. */
92
+ cache: Map<string, Record<string, unknown>>;
93
+ /** Tables currently being inserted. A repeat is a factory cycle. */
94
+ readonly creating: Set<string>;
95
+ readonly created: Map<string, number>;
96
+ readonly definitions: Map<string, (context: FactoryContext) => Readonly<Record<string, unknown>>>;
97
+ };
98
+ /**
99
+ * Builds the factory object for `definitions`.
100
+ *
101
+ * @param host - Schema, client, and seed
102
+ * @param definitions - One function per table
103
+ * @returns A factory per definition
104
+ */
105
+ export declare function bindFactories(host: FactoryHost, definitions: Readonly<Record<string, (context: FactoryContext) => Readonly<Record<string, unknown>>>>): Record<string, TableFactory>;
106
+ /**
107
+ * Inserts one row, filling required columns. Used by factories and by isolation.
108
+ *
109
+ * @param host - Schema, client, and seed
110
+ * @param table - Table name
111
+ * @param overrides - Column values and the tenant key
112
+ * @param children - Child counts from `with`
113
+ * @param inherited - Tenant from the parent row, when this row is a child
114
+ * @returns The inserted row
115
+ */
116
+ export declare function insertRow(host: FactoryHost, table: string, overrides: Readonly<Record<string, unknown>> | undefined, children: Readonly<Record<string, number>> | undefined, inherited: string | undefined): Promise<Record<string, unknown>>;
117
+ export {};
@@ -0,0 +1,75 @@
1
+ /**
2
+ * What a factory needs from a compiled schema: required columns, foreign keys,
3
+ * and which tables are tenant or global.
4
+ *
5
+ * Column tenancy is the only strategy in this version. A table is tenant when
6
+ * its predicate binds a scope, and global when `global("reason")` opted it out.
7
+ * `schemaPerTenant` and `databasePerTenant` are not in 0.4.
8
+ */
9
+ import type { Catalog } from "../../contracts/catalog/types.js";
10
+ import type { QuerySchema, RelationModel } from "../../dialects/pg/model.js";
11
+ /** A schema `testing()` can open. `schema()` returns this shape. */
12
+ export type TestingSchema = QuerySchema & {
13
+ readonly catalog: Catalog;
14
+ };
15
+ /** One foreign key, SQL names, child columns aligned with the target columns. */
16
+ export type ForeignKey = {
17
+ readonly columns: readonly string[];
18
+ readonly target: string;
19
+ readonly targetColumns: readonly string[];
20
+ };
21
+ /** One column, with the catalog facts the factory uses to fill a default. */
22
+ export type ColumnFacts = {
23
+ readonly field: string;
24
+ readonly sql: string;
25
+ readonly dataType: string;
26
+ readonly nullable: boolean;
27
+ readonly databaseDefault: boolean;
28
+ readonly identity: boolean;
29
+ readonly generated: boolean;
30
+ readonly guarded: boolean;
31
+ readonly writable: boolean;
32
+ readonly fill: boolean;
33
+ readonly accepts: readonly string[] | undefined;
34
+ readonly encode: (value: unknown) => string;
35
+ };
36
+ /** One table. */
37
+ export type TableFacts = {
38
+ readonly name: string;
39
+ readonly sql: string;
40
+ readonly primary: readonly string[];
41
+ readonly columns: readonly ColumnFacts[];
42
+ readonly relations: readonly RelationModel[];
43
+ readonly foreignKeys: readonly ForeignKey[];
44
+ /** Column tenancy applies a predicate. */
45
+ readonly tenant: boolean;
46
+ /** `global("reason")`. Absent on a tenant table and on a schema with no tenancy. */
47
+ readonly globalReason: string | undefined;
48
+ };
49
+ /**
50
+ * Scope value used to ask a tenancy object whether a table is tenant.
51
+ *
52
+ * A test tenancy can recognise this probe and still omit the predicate on a
53
+ * real query. That is how the isolation check is shown to fail.
54
+ */
55
+ export declare const tenantProbe = "00000000-0000-4000-8000-000000000001";
56
+ /**
57
+ * Reads every table in declaration order.
58
+ *
59
+ * Views and materialized views are in the query model too. They have no rows
60
+ * of their own to insert, so they are left out.
61
+ *
62
+ * @param schema - Compiled schema
63
+ * @returns One facts object per table
64
+ */
65
+ export declare function readFacts(schema: TestingSchema): readonly TableFacts[];
66
+ /**
67
+ * A column the factory must set when the definition and the overrides omit it.
68
+ *
69
+ * Nullable, a database default, identity, a generated expression, a client
70
+ * fill, and a guarded column are left to the database or the scope (D199).
71
+ *
72
+ * @param column - Column facts
73
+ * @returns Whether the factory fills it
74
+ */
75
+ export declare function isRequired(column: ColumnFacts): boolean;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * `okmodel/testing`: a real database, factories, query counts, and the
3
+ * isolation check (spec §20, D199).
4
+ *
5
+ * The database is never mocked. Pass a driver pool from `open()`.
6
+ */
7
+ import type { DriverPool } from "../../contracts/driver.js";
8
+ import type { Connected } from "../../runtime/types.js";
9
+ import { type TableFactory } from "./factories.js";
10
+ import { type TestingSchema } from "./facts.js";
11
+ import { type IsolationReport } from "./isolation.js";
12
+ /** Options for {@link testing}. */
13
+ export type TestingOptions = {
14
+ /**
15
+ * A driver pool, or a promise of one.
16
+ *
17
+ * `open()` from `okmodel/pg/pglite` and `open({ url })` from
18
+ * `okmodel/pg/postgresjs` both satisfy this.
19
+ */
20
+ readonly driver: DriverPool | Promise<DriverPool>;
21
+ /**
22
+ * Seed for factory generators.
23
+ *
24
+ * The default is `1`. The same seed replays the same values.
25
+ */
26
+ readonly seed?: number;
27
+ /**
28
+ * Apply the schema's catalog before connecting.
29
+ *
30
+ * The default is true, for an empty test database. `okm seed` passes false:
31
+ * the target already has its schema.
32
+ */
33
+ readonly migrate?: boolean;
34
+ };
35
+ /**
36
+ * What a factory definition may call.
37
+ *
38
+ * Re-exported so a seed file can name the argument.
39
+ */
40
+ export type { FactoryContext, TableFactory } from "./factories.js";
41
+ export type { IsolationReport } from "./isolation.js";
42
+ /** Definitions passed to {@link Testing.factories}. */
43
+ export type FactoryDefinitions = {
44
+ readonly [table: string]: (context: import("./factories.js").FactoryContext) => Readonly<Record<string, unknown>>;
45
+ };
46
+ /**
47
+ * A connected test database.
48
+ *
49
+ * @typeParam S - Schema passed to {@link testing}
50
+ */
51
+ export type Testing<S extends TestingSchema = TestingSchema> = {
52
+ /** The client. `close` on the harness closes this pool. */
53
+ readonly db: Connected<S>;
54
+ /**
55
+ * Builds factories for the named tables.
56
+ *
57
+ * @param definitions - One function per table
58
+ * @returns `create`, `createMany`, and `with` for each table
59
+ */
60
+ factories<const D extends FactoryDefinitions>(definitions: D): {
61
+ readonly [K in keyof D]: TableFactory;
62
+ };
63
+ /**
64
+ * Fails when `run` sends a different number of statements than `count`.
65
+ *
66
+ * Transaction control is not counted. The error lists the statements that were.
67
+ *
68
+ * @param count - Expected statements
69
+ * @param run - The work to count
70
+ */
71
+ expectQueries(count: number, run: () => Promise<unknown>): Promise<void>;
72
+ /**
73
+ * Runs the cross-tenant check.
74
+ *
75
+ * @returns Checked tenant tables and skipped global tables
76
+ */
77
+ isolation(): Promise<IsolationReport>;
78
+ /** Closes the client and the pool. */
79
+ close(): Promise<void>;
80
+ };
81
+ /**
82
+ * Opens `schema` on `driver` and returns the test harness.
83
+ *
84
+ * @typeParam S - Schema
85
+ * @param schema - A `schema()` value
86
+ * @param options - Driver, seed, and whether to apply the catalog
87
+ * @returns The harness
88
+ */
89
+ export declare function testing<const S extends TestingSchema>(schema: S, options: TestingOptions): Promise<Testing<S>>;
@@ -0,0 +1,8 @@
1
+ import {
2
+ testing2
3
+ } from "../../shared/g516zdsb.js";
4
+ import"../../shared/8xsbagad.js";
5
+ import"../../shared/8bwbwy8h.js";
6
+ export {
7
+ testing2 as testing
8
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Cross-tenant isolation check.
3
+ *
4
+ * For every tenant table, insert one row as tenant B and run the basic reads
5
+ * and writes as tenant A. A returned row, a count above zero, or an update or
6
+ * delete that touches a row is a leak. Global tables are skipped and listed.
7
+ * Column tenancy is the strategy this version has. A table is tenant when its
8
+ * predicate says so (D199).
9
+ */
10
+ import { type FactoryHost } from "./factories.js";
11
+ /** What {@link isolation} checked, and the global tables it skipped. */
12
+ export type IsolationReport = {
13
+ /** Tenant tables whose queries stayed inside tenant A. */
14
+ readonly checked: readonly string[];
15
+ /** Global tables, with the reason from `global("reason")`. */
16
+ readonly skipped: readonly {
17
+ readonly table: string;
18
+ readonly reason: string;
19
+ }[];
20
+ };
21
+ /**
22
+ * Runs the check on `host`.
23
+ *
24
+ * The cache used for `x.ref` is set aside for the run, so a row the test
25
+ * already inserted is not reused as tenant B's row.
26
+ *
27
+ * @param host - The open harness
28
+ * @returns The tables that were checked and the global tables that were not
29
+ */
30
+ export declare function isolation(host: FactoryHost): Promise<IsolationReport>;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Seeded generators for factories.
3
+ *
4
+ * Mulberry32, a fixed word list, and a counter. No runtime dependency.
5
+ * The same seed replays the same sequence (D199).
6
+ */
7
+ /** A seeded sequence and the values factories ask for. */
8
+ export type Rng = {
9
+ /** Next unit interval, `[0, 1)`. */
10
+ next(): number;
11
+ /** Integer in `[0, bound)`. */
12
+ int(bound: number): number;
13
+ /** A version-4 UUID. The bits come from this sequence. */
14
+ uuid(): string;
15
+ /** `local@example.com`, unique for this sequence. */
16
+ email(): string;
17
+ /** A first and last name from the fixed lists. */
18
+ name(): string;
19
+ /** `count` words from the fixed list. */
20
+ words(count: number): string;
21
+ /** A boolean. */
22
+ boolean(): boolean;
23
+ /** A number in `[0, 1)`. */
24
+ float(): number;
25
+ /** An ISO date `YYYY-MM-DD` in 2020. */
26
+ date(): string;
27
+ /** One value from `list`. */
28
+ pick<T>(list: readonly T[]): T;
29
+ /** The next counter value, starting at 1. Text defaults append it. */
30
+ counter(): number;
31
+ };
32
+ /**
33
+ * Builds the sequence for `seed`.
34
+ *
35
+ * Seed `1` is the default. `0` is shifted so the first step is not a fixed point.
36
+ *
37
+ * @param seed - Integer seed
38
+ * @returns The sequence
39
+ */
40
+ export declare function rng(seed: number): Rng;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A pool that records every statement sent to the driver.
3
+ *
4
+ * `expectQueries` counts this log. Transaction control is left out (D199).
5
+ * The runtime logger records errors only, and `hookm` records notices and
6
+ * transaction phases, not statement text, so the count wraps the pool.
7
+ */
8
+ import type { DriverPool, WireValue } from "../../contracts/driver.js";
9
+ /** One statement that reached the driver. */
10
+ export type Recorded = {
11
+ readonly text: string;
12
+ readonly params: readonly WireValue[];
13
+ };
14
+ /** A recording pool and its log. */
15
+ export type Recording = {
16
+ readonly pool: DriverPool;
17
+ /** Every statement since the pool was wrapped, in the order sent. */
18
+ readonly log: Recorded[];
19
+ };
20
+ /**
21
+ * Reports whether `text` only frames a transaction.
22
+ *
23
+ * Not counted: `BEGIN`, `START TRANSACTION`, `COMMIT`, `END`, `ROLLBACK`,
24
+ * `ROLLBACK TO`, `SAVEPOINT`, `RELEASE SAVEPOINT`, `SET TRANSACTION`,
25
+ * `SET LOCAL`, and `SET SESSION CHARACTERISTICS`. A data statement is counted,
26
+ * including one that runs inside a transaction.
27
+ *
28
+ * @param text - Statement text
29
+ * @returns Whether the statement is transaction control
30
+ */
31
+ export declare function isTransactionControl(text: string): boolean;
32
+ /**
33
+ * Wraps a pool so each statement is logged.
34
+ *
35
+ * @param pool - The real pool
36
+ * @returns The wrapped pool and its log
37
+ */
38
+ export declare function record(pool: DriverPool): Recording;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okmodel",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "okmodel is a catalog-first TypeScript ORM, PostgreSQL first.",
5
5
  "keywords": [
6
6
  "migrations",
@@ -82,6 +82,10 @@
82
82
  "types": "./dist/runtime/tenancy/index.d.ts",
83
83
  "import": "./dist/runtime/tenancy/index.js"
84
84
  },
85
+ "./testing": {
86
+ "types": "./dist/tooling/testing/index.d.ts",
87
+ "import": "./dist/tooling/testing/index.js"
88
+ },
85
89
  "./validate": {
86
90
  "types": "./dist/runtime/validate/index.d.ts",
87
91
  "import": "./dist/runtime/validate/index.js"
@@ -108,7 +112,7 @@
108
112
  }
109
113
  },
110
114
  "scripts": {
111
- "build": "rm -rf dist && bun build ./src/contracts/index.ts ./src/contracts/internal.ts ./src/dialects/pg/index.ts ./src/runtime/pg/postgresjs.ts ./src/runtime/pg/pglite.ts ./src/runtime/pg/pg.ts ./src/runtime/pg/bun.ts ./src/runtime/ids/index.ts ./src/runtime/safety/index.ts ./src/runtime/traits/index.ts ./src/runtime/tenancy/index.ts ./src/runtime/validate/index.ts ./src/dialects/pg/ext/index.ts ./src/dialects/pg/fn/index.ts ./src/dialects/pg/view/index.ts ./src/dialects/pg/ext/citext.ts ./src/dialects/pg/ext/pg-trgm.ts ./src/tooling/migrate/index.ts ./src/tooling/cli.ts --outdir ./dist --root ./src --format esm --target node --splitting --chunk-naming=shared/[hash].js --external postgres --external @electric-sql/pglite --external pg --external bun && bunx tsc -p tsconfig.build.json && bun ./scripts/write-bin-wrappers.ts",
115
+ "build": "rm -rf dist && bun build ./src/contracts/index.ts ./src/contracts/internal.ts ./src/dialects/pg/index.ts ./src/runtime/pg/postgresjs.ts ./src/runtime/pg/pglite.ts ./src/runtime/pg/pg.ts ./src/runtime/pg/bun.ts ./src/runtime/ids/index.ts ./src/runtime/safety/index.ts ./src/runtime/traits/index.ts ./src/runtime/tenancy/index.ts ./src/runtime/validate/index.ts ./src/dialects/pg/ext/index.ts ./src/dialects/pg/fn/index.ts ./src/dialects/pg/view/index.ts ./src/dialects/pg/ext/citext.ts ./src/dialects/pg/ext/pg-trgm.ts ./src/tooling/migrate/index.ts ./src/tooling/testing/index.ts ./src/tooling/cli.ts --outdir ./dist --root ./src --format esm --target node --splitting --chunk-naming=shared/[hash].js --external postgres --external @electric-sql/pglite --external pg --external bun && bunx tsc -p tsconfig.build.json && bun ./scripts/write-bin-wrappers.ts",
112
116
  "driver-bench": "bun ./scripts/driver-bench.ts",
113
117
  "typecheck": "bunx tsc --noEmit -p tsconfig.json",
114
118
  "editor-check": "bun ./scripts/editor-check.ts",