@telorun/sql 0.21.3 → 0.22.1
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/index.d.ts +18 -2
- package/dist/index.js +9 -2
- package/dist/schema/declaration-snapshot.d.ts +21 -0
- package/dist/schema/declaration-snapshot.js +46 -0
- package/dist/schema/declared-schema.d.ts +64 -0
- package/dist/schema/declared-schema.js +16 -0
- package/dist/schema/migration-runner.d.ts +30 -0
- package/dist/schema/migration-runner.js +38 -0
- package/dist/schema/normalize-table.d.ts +57 -0
- package/dist/schema/normalize-table.js +125 -0
- package/dist/schema/reclaim-policy.d.ts +34 -0
- package/dist/schema/reclaim-policy.js +40 -0
- package/dist/schema/schema-driver.d.ts +184 -0
- package/dist/schema/schema-driver.js +1 -0
- package/dist/schema/schema-ledger.d.ts +119 -0
- package/dist/schema/schema-ledger.js +231 -0
- package/dist/schema/schema-reconciler.d.ts +45 -0
- package/dist/schema/schema-reconciler.js +276 -0
- package/dist/schema/schema-run.d.ts +50 -0
- package/dist/schema/schema-run.js +318 -0
- package/dist/schema/table-reference.d.ts +25 -0
- package/dist/schema/table-reference.js +61 -0
- package/dist/sql-connection-base.d.ts +21 -0
- package/dist/sql-connection-base.js +30 -4
- package/dist/sql-connection.d.ts +19 -0
- package/package.json +5 -3
- package/src/index.ts +43 -2
- package/src/schema/declaration-snapshot.ts +71 -0
- package/src/schema/declared-schema.ts +73 -0
- package/src/schema/migration-runner.ts +69 -0
- package/src/schema/normalize-table.ts +224 -0
- package/src/schema/reclaim-policy.ts +73 -0
- package/src/schema/schema-driver.ts +207 -0
- package/src/schema/schema-ledger.ts +309 -0
- package/src/schema/schema-reconciler.ts +372 -0
- package/src/schema/schema-run.ts +441 -0
- package/src/schema/table-reference.ts +78 -0
- package/src/sql-connection-base.ts +35 -4
- package/src/sql-connection.ts +21 -0
- package/dist/sql-migration-controller.d.ts +0 -16
- package/dist/sql-migration-controller.js +0 -13
- package/dist/sql-migrations-controller.d.ts +0 -23
- package/dist/sql-migrations-controller.js +0 -98
- package/src/sql-migration-controller.ts +0 -20
- package/src/sql-migrations-controller.ts +0 -143
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DeclaredColumn,
|
|
3
|
+
DeclaredForeignKey,
|
|
4
|
+
DeclaredIndex,
|
|
5
|
+
DeclaredTable,
|
|
6
|
+
} from "./declared-schema.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The manifest shape a backend's `Table` kind declares, reduced to the
|
|
10
|
+
* normalized model.
|
|
11
|
+
*
|
|
12
|
+
* The STRUCTURE is shared — named columns, named indexes, named foreign keys —
|
|
13
|
+
* while the type vocabulary is not: `type` is carried through opaquely and
|
|
14
|
+
* every field the shared model does not name becomes a type parameter, so a
|
|
15
|
+
* backend adds `length`, `precision` or `collation` to its schema and nothing
|
|
16
|
+
* here changes. Types are structured rather than spelled into a scalar
|
|
17
|
+
* (`type: varchar` with `length: 64`, never `varchar(64)`), so nothing has to
|
|
18
|
+
* parse a type back apart.
|
|
19
|
+
*/
|
|
20
|
+
export interface RawColumn {
|
|
21
|
+
readonly type: string;
|
|
22
|
+
readonly nullable?: boolean;
|
|
23
|
+
readonly array?: boolean;
|
|
24
|
+
readonly primaryKey?: boolean;
|
|
25
|
+
readonly unique?: boolean;
|
|
26
|
+
readonly default?: unknown;
|
|
27
|
+
readonly defaultExpression?: string;
|
|
28
|
+
readonly identity?: string;
|
|
29
|
+
readonly renamedFrom?: string;
|
|
30
|
+
readonly [param: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RawIndex {
|
|
34
|
+
readonly columns: readonly string[];
|
|
35
|
+
readonly unique?: boolean;
|
|
36
|
+
readonly [option: string]: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RawForeignKey {
|
|
40
|
+
readonly columns: readonly string[];
|
|
41
|
+
readonly references: { readonly table: unknown; readonly columns: readonly string[] };
|
|
42
|
+
readonly onDelete?: string;
|
|
43
|
+
readonly onUpdate?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RawTable {
|
|
47
|
+
readonly table: string;
|
|
48
|
+
readonly columns?: Record<string, RawColumn>;
|
|
49
|
+
readonly indexes?: Record<string, RawIndex>;
|
|
50
|
+
readonly foreignKeys?: Record<string, RawForeignKey>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const COLUMN_KEYS = new Set([
|
|
54
|
+
"type",
|
|
55
|
+
"nullable",
|
|
56
|
+
"array",
|
|
57
|
+
"primaryKey",
|
|
58
|
+
"unique",
|
|
59
|
+
"default",
|
|
60
|
+
"defaultExpression",
|
|
61
|
+
"identity",
|
|
62
|
+
"renamedFrom",
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
const INDEX_KEYS = new Set(["columns", "unique"]);
|
|
66
|
+
|
|
67
|
+
function params(raw: Record<string, unknown>, known: ReadonlySet<string>): Record<string, unknown> {
|
|
68
|
+
return Object.fromEntries(
|
|
69
|
+
Object.entries(raw).filter(([key, value]) => !known.has(key) && value !== undefined),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeColumn(name: string, raw: RawColumn): DeclaredColumn {
|
|
74
|
+
if (raw.default !== undefined && raw.defaultExpression !== undefined) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`column '${name}' declares both 'default' and 'defaultExpression' — a typed literal and ` +
|
|
77
|
+
`a backend SQL expression are separate fields and exactly one may be set`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// A primary key and an identity column cannot hold NULL, and the engine
|
|
82
|
+
// enforces that whether or not the declaration says so. Left at the `nullable`
|
|
83
|
+
// default of true, the column would read back NOT NULL on the next boot, the
|
|
84
|
+
// pass would see a difference it could "fix", and every boot from then on
|
|
85
|
+
// would try to DROP NOT NULL on a primary key and fail. So the implication is
|
|
86
|
+
// applied here, once, where both the DDL and the comparison read it — and a
|
|
87
|
+
// declaration that states the opposite is refused rather than quietly
|
|
88
|
+
// overruled.
|
|
89
|
+
const impliesNotNull = raw.primaryKey === true || raw.identity !== undefined;
|
|
90
|
+
if (impliesNotNull && raw.nullable === true) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`column '${name}' is declared nullable and ${raw.primaryKey ? "a primary key" : "an identity column"}, ` +
|
|
93
|
+
`which cannot hold NULL. Remove 'nullable: true'.`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
name,
|
|
99
|
+
type: raw.type,
|
|
100
|
+
params: params(raw as Record<string, unknown>, COLUMN_KEYS),
|
|
101
|
+
nullable: impliesNotNull ? false : (raw.nullable ?? true),
|
|
102
|
+
array: raw.array ?? false,
|
|
103
|
+
primaryKey: raw.primaryKey ?? false,
|
|
104
|
+
unique: raw.unique ?? false,
|
|
105
|
+
default: raw.default,
|
|
106
|
+
defaultExpression: raw.defaultExpression,
|
|
107
|
+
identity: raw.identity,
|
|
108
|
+
renamedFrom: raw.renamedFrom,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Turns whatever sits at a `references.table` slot into the referenced table's
|
|
114
|
+
* physical name.
|
|
115
|
+
*
|
|
116
|
+
* REQUIRED rather than optional: a `!ref` is not resolved when a controller is
|
|
117
|
+
* constructed — Phase-5 injection runs after `create()` returns — so a caller
|
|
118
|
+
* that omitted one would read the sentinel and reproduce, silently, the exact
|
|
119
|
+
* defect this parameter exists to fix. `tableReferenceResolver` is the one every
|
|
120
|
+
* backend uses.
|
|
121
|
+
*/
|
|
122
|
+
export type TableReferenceResolver = (value: unknown, fk: string) => string;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Structural checks over one declaration, at resource creation — before any
|
|
126
|
+
* connection is opened, let alone any DDL planned.
|
|
127
|
+
*
|
|
128
|
+
* Each of these would otherwise reach the engine as raw SQL and come back as a
|
|
129
|
+
* driver error naming a statement the author never wrote. They are decidable
|
|
130
|
+
* from the declaration alone, so they are decided here and reported against the
|
|
131
|
+
* field that is wrong.
|
|
132
|
+
*/
|
|
133
|
+
function validateTable(table: DeclaredTable): void {
|
|
134
|
+
const where = `${table.name}`;
|
|
135
|
+
if (table.columns.length === 0) {
|
|
136
|
+
throw new Error(`table '${where}' declares no columns — a table needs at least one.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const names = new Set(table.columns.map((c) => c.name));
|
|
140
|
+
|
|
141
|
+
const primaryKeys = table.columns.filter((c) => c.primaryKey).map((c) => c.name);
|
|
142
|
+
if (primaryKeys.length > 1) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`table '${where}' marks ${primaryKeys.map((n) => `'${n}'`).join(" and ")} as primaryKey. ` +
|
|
145
|
+
`A composite primary key is not expressible as a per-column flag — declare one column ` +
|
|
146
|
+
`as the key, or create the constraint in a 'migrations:' entry.`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const column of table.columns) {
|
|
151
|
+
if (!column.renamedFrom) continue;
|
|
152
|
+
if (column.renamedFrom === column.name) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`column '${where}.${column.name}' declares renamedFrom itself, which describes no rename.`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (names.has(column.renamedFrom)) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`column '${where}.${column.name}' renames from '${column.renamedFrom}', which this table ` +
|
|
160
|
+
`also declares. A rename's source is the column being retired, so declaring both would ` +
|
|
161
|
+
`copy one live column into another and retire neither.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// An index or foreign key over a column the table does not declare cannot be
|
|
167
|
+
// created, and the engine's complaint would name a generated statement.
|
|
168
|
+
for (const index of table.indexes) {
|
|
169
|
+
for (const column of index.columns) {
|
|
170
|
+
if (!names.has(column)) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`index '${where}.${index.name}' names column '${column}', which this table does not ` +
|
|
173
|
+
`declare.`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
for (const fk of table.foreignKeys) {
|
|
179
|
+
for (const column of fk.columns) {
|
|
180
|
+
if (!names.has(column)) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`foreign key '${where}.${fk.name}' names column '${column}', which this table does not ` +
|
|
183
|
+
`declare.`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (fk.references.columns.length !== fk.columns.length) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`foreign key '${where}.${fk.name}' has ${fk.columns.length} column(s) but references ` +
|
|
190
|
+
`${fk.references.columns.length} — a foreign key maps its columns one for one.`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function normalizeTable(
|
|
197
|
+
raw: RawTable,
|
|
198
|
+
resolveReference: TableReferenceResolver,
|
|
199
|
+
): DeclaredTable {
|
|
200
|
+
const columns = Object.entries(raw.columns ?? {}).map(([name, column]) =>
|
|
201
|
+
normalizeColumn(name, column),
|
|
202
|
+
);
|
|
203
|
+
const indexes: DeclaredIndex[] = Object.entries(raw.indexes ?? {}).map(([name, index]) => ({
|
|
204
|
+
name,
|
|
205
|
+
columns: [...index.columns],
|
|
206
|
+
unique: index.unique ?? false,
|
|
207
|
+
options: params(index as Record<string, unknown>, INDEX_KEYS),
|
|
208
|
+
}));
|
|
209
|
+
const foreignKeys: DeclaredForeignKey[] = Object.entries(raw.foreignKeys ?? {}).map(
|
|
210
|
+
([name, fk]) => ({
|
|
211
|
+
name,
|
|
212
|
+
columns: [...fk.columns],
|
|
213
|
+
references: {
|
|
214
|
+
table: resolveReference(fk.references.table, name),
|
|
215
|
+
columns: [...fk.references.columns],
|
|
216
|
+
},
|
|
217
|
+
onDelete: fk.onDelete,
|
|
218
|
+
onUpdate: fk.onUpdate,
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
const table: DeclaredTable = { name: raw.table, columns, indexes, foreignKeys };
|
|
222
|
+
validateTable(table);
|
|
223
|
+
return table;
|
|
224
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { parseDurationMs } from "@telorun/sdk";
|
|
2
|
+
import type { TombstoneRecord, VersionRecord } from "./schema-ledger.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* When a tombstone may be reclaimed. Eligibility is a CONJUNCTION: N released
|
|
6
|
+
* versions must have been observed since the object went missing, AND T must
|
|
7
|
+
* have elapsed. Version is the primary signal — it is what proves older code is
|
|
8
|
+
* no longer live — and time is the backstop, because N versions can land in an
|
|
9
|
+
* afternoon.
|
|
10
|
+
*
|
|
11
|
+
* Declaring no policy means nothing is ever dropped, so reclamation is opt-in
|
|
12
|
+
* by declaration rather than by invocation.
|
|
13
|
+
*/
|
|
14
|
+
export interface ReclaimPolicy {
|
|
15
|
+
readonly afterVersions: number;
|
|
16
|
+
readonly afterDuration: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Eligibility {
|
|
20
|
+
readonly eligible: boolean;
|
|
21
|
+
/** Versions observed since the tombstone, and how many are still needed. */
|
|
22
|
+
readonly versionsObserved: number;
|
|
23
|
+
readonly versionsRemaining: number;
|
|
24
|
+
readonly msElapsed: number;
|
|
25
|
+
readonly msRemaining: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A rollback resets progress rather than merely pausing it.
|
|
30
|
+
*
|
|
31
|
+
* Going backwards proves older code is live, so a boot at a version that was
|
|
32
|
+
* already observed at or before the tombstone is not one more release past the
|
|
33
|
+
* removal — it is evidence the removal is not yet safe. Both counters restart
|
|
34
|
+
* from that observation: the version count, and the elapsed-time baseline. This
|
|
35
|
+
* is answerable only because the ledger records the observed *sequence* rather
|
|
36
|
+
* than a counter.
|
|
37
|
+
*/
|
|
38
|
+
export function assessTombstone(
|
|
39
|
+
tombstone: TombstoneRecord,
|
|
40
|
+
history: readonly VersionRecord[],
|
|
41
|
+
policy: ReclaimPolicy,
|
|
42
|
+
nowMs: number,
|
|
43
|
+
): Eligibility {
|
|
44
|
+
const priorVersions = new Set(
|
|
45
|
+
history
|
|
46
|
+
.filter((entry) => entry.sequence <= tombstone.missingSinceSequence)
|
|
47
|
+
.map((entry) => entry.version),
|
|
48
|
+
);
|
|
49
|
+
let counted = new Set<string>();
|
|
50
|
+
let baselineAt = tombstone.missingSinceAt;
|
|
51
|
+
for (const entry of history) {
|
|
52
|
+
if (entry.sequence <= tombstone.missingSinceSequence) continue;
|
|
53
|
+
if (priorVersions.has(entry.version)) {
|
|
54
|
+
counted = new Set();
|
|
55
|
+
baselineAt = entry.firstSeenAt;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
counted.add(entry.version);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const versionsObserved = counted.size;
|
|
62
|
+
const msElapsed = Math.max(0, nowMs - Date.parse(baselineAt));
|
|
63
|
+
const requiredMs = parseDurationMs(policy.afterDuration);
|
|
64
|
+
const versionsRemaining = Math.max(0, policy.afterVersions - versionsObserved);
|
|
65
|
+
const msRemaining = Math.max(0, requiredMs - msElapsed);
|
|
66
|
+
return {
|
|
67
|
+
eligible: versionsRemaining === 0 && msRemaining === 0,
|
|
68
|
+
versionsObserved,
|
|
69
|
+
versionsRemaining,
|
|
70
|
+
msElapsed,
|
|
71
|
+
msRemaining,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { SqlConnection } from "../sql-connection.js";
|
|
2
|
+
import type { LedgerTables } from "./schema-ledger.js";
|
|
3
|
+
import type {
|
|
4
|
+
SchemaObjectId,
|
|
5
|
+
DeclaredColumn,
|
|
6
|
+
DeclaredForeignKey,
|
|
7
|
+
DeclaredIndex,
|
|
8
|
+
DeclaredTable,
|
|
9
|
+
} from "./declared-schema.js";
|
|
10
|
+
|
|
11
|
+
/** The live shape of one table, as the driver reads it back from the database. */
|
|
12
|
+
export interface LiveColumn {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
/**
|
|
15
|
+
* The driver's canonical rendering of the column's type, compared verbatim
|
|
16
|
+
* against what {@link SchemaDriver.typeSignature} produces for a declaration.
|
|
17
|
+
* Comparing signatures rather than parsed parts is what keeps every type rule
|
|
18
|
+
* inside the driver.
|
|
19
|
+
*/
|
|
20
|
+
readonly typeSignature: string;
|
|
21
|
+
readonly nullable: boolean;
|
|
22
|
+
readonly hasDefault: boolean;
|
|
23
|
+
/** Whether the column is part of the table's primary key. */
|
|
24
|
+
readonly primaryKey: boolean;
|
|
25
|
+
/** Whether a single-column uniqueness constraint covers it. */
|
|
26
|
+
readonly unique: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* An index as the database has it, not merely its name.
|
|
31
|
+
*
|
|
32
|
+
* Compared by name alone, changing which columns an index covers — or making it
|
|
33
|
+
* unique — emitted nothing and reported nothing, while the ledger recorded the
|
|
34
|
+
* new definition as owned. The declaration then asserted an index the database
|
|
35
|
+
* was not providing.
|
|
36
|
+
*/
|
|
37
|
+
export interface LiveIndex {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly columns: readonly string[];
|
|
40
|
+
readonly unique: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A foreign key as the database has it. Same reasoning as {@link LiveIndex}:
|
|
44
|
+
* changing `onDelete` is a change to what the constraint DOES. */
|
|
45
|
+
export interface LiveForeignKey {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly columns: readonly string[];
|
|
48
|
+
readonly references: { readonly table: string; readonly columns: readonly string[] };
|
|
49
|
+
readonly onDelete?: string;
|
|
50
|
+
readonly onUpdate?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface LiveTable {
|
|
54
|
+
readonly name: string;
|
|
55
|
+
readonly columns: readonly LiveColumn[];
|
|
56
|
+
readonly indexes: readonly LiveIndex[];
|
|
57
|
+
readonly foreignKeys: readonly LiveForeignKey[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** How a declared change relates to the data already in the column. */
|
|
61
|
+
export type ChangeSafety =
|
|
62
|
+
| { readonly safe: true }
|
|
63
|
+
| { readonly safe: false; readonly reason: string };
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Everything reconciliation needs that only the engine knows: how a type is
|
|
67
|
+
* spelled, how it is read back, what a lock is, and which alterations the
|
|
68
|
+
* engine can perform at all. The diff, the ledger, the tombstones and the
|
|
69
|
+
* ordering live in the shared library and are identical for every backend.
|
|
70
|
+
*
|
|
71
|
+
* Statement renderers return SQL text; the runner executes and logs it. They
|
|
72
|
+
* never touch the connection themselves, so a plan can be built, reported and
|
|
73
|
+
* refused without anything having run.
|
|
74
|
+
*/
|
|
75
|
+
export interface SchemaDriver {
|
|
76
|
+
readonly connection: SqlConnection;
|
|
77
|
+
|
|
78
|
+
/** Quote an identifier. */
|
|
79
|
+
quote(name: string): string;
|
|
80
|
+
/** Qualify a table with the namespace this schema resource owns. */
|
|
81
|
+
qualify(schema: string, table: string): string;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Hold the engine's cross-process schema lock for the duration of `body`.
|
|
85
|
+
* Every replica of an app boots the same pass, so this is what makes
|
|
86
|
+
* "reconcile once" true rather than hoped for.
|
|
87
|
+
*/
|
|
88
|
+
withLock<T>(schema: string, body: () => Promise<T>): Promise<T>;
|
|
89
|
+
|
|
90
|
+
/** Create the namespace when absent. Purely additive. */
|
|
91
|
+
ensureNamespaceStatements(schema: string): string[];
|
|
92
|
+
|
|
93
|
+
/** The three ledger tables, `CREATE TABLE IF NOT EXISTS`. Named by the caller:
|
|
94
|
+
* which ledger a schema resource writes to is its own declaration. */
|
|
95
|
+
ledgerStatements(schema: string, tables: LedgerTables): string[];
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Run `statements` atomically where the engine allows DDL inside a
|
|
99
|
+
* transaction, and sequentially where it does not. Which of the two an engine
|
|
100
|
+
* offers is the driver's to know; the runner only needs the strongest
|
|
101
|
+
* grouping available, and relies on the pass being idempotent for the rest.
|
|
102
|
+
*
|
|
103
|
+
* A migration's ledger row is the LAST statement of its group, so on an engine
|
|
104
|
+
* with transactional DDL "applied" and "recorded as applied" are one commit.
|
|
105
|
+
* An implementation whose engine cannot do that MUST say so here, because the
|
|
106
|
+
* consequence is specific: a crash mid-group can re-run a migration that is
|
|
107
|
+
* not idempotent.
|
|
108
|
+
*/
|
|
109
|
+
runAtomically(statements: readonly string[]): Promise<void>;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The current time, as ISO-8601 UTC text, **from the database**.
|
|
113
|
+
*
|
|
114
|
+
* Not the application's clock: the grace window gates an irreversible drop,
|
|
115
|
+
* and one replica with a skewed clock would satisfy `afterDuration`
|
|
116
|
+
* instantly. The ledger's history already treats the database as the
|
|
117
|
+
* authority; the clock measured against it has to come from there too, so
|
|
118
|
+
* every replica measures the same elapsed time.
|
|
119
|
+
*/
|
|
120
|
+
now(): Promise<string>;
|
|
121
|
+
|
|
122
|
+
/** Read back the live shape of the named tables. Absent tables are omitted. */
|
|
123
|
+
introspect(schema: string, tables: readonly string[]): Promise<LiveTable[]>;
|
|
124
|
+
|
|
125
|
+
/** The canonical signature of a declared column's type, for comparison with {@link LiveColumn.typeSignature}. */
|
|
126
|
+
typeSignature(column: DeclaredColumn): string;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Whether altering a live column to the declared one can be applied without
|
|
130
|
+
* risking the data already in it. Type widening is safe, narrowing is not,
|
|
131
|
+
* and only the engine knows which is which.
|
|
132
|
+
*/
|
|
133
|
+
classifyAlter(live: LiveColumn, declared: DeclaredColumn): ChangeSafety;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Whether the values in a live column can be COPIED into a newly added one —
|
|
137
|
+
* the expand-contract half of a rename.
|
|
138
|
+
*
|
|
139
|
+
* A different question from {@link classifyAlter}, which asks whether a column
|
|
140
|
+
* can be changed in place. Here the target is brand new, so its nullability
|
|
141
|
+
* and default are already what the declaration says and only the VALUES have
|
|
142
|
+
* to survive the move. An engine that can alter nothing in place can still
|
|
143
|
+
* copy freely between compatible types, and one that copies between anything
|
|
144
|
+
* may still lose data doing it.
|
|
145
|
+
*/
|
|
146
|
+
classifyCopy(live: LiveColumn, target: DeclaredColumn): ChangeSafety;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Whether an index can be brought to its declaration in place. Most engines
|
|
150
|
+
* cannot alter one, so the honest answer is usually to drop and recreate —
|
|
151
|
+
* which this returns as statements, or refuses when the index is not this
|
|
152
|
+
* schema's to rebuild.
|
|
153
|
+
*/
|
|
154
|
+
classifyIndexChange(live: LiveIndex, declared: DeclaredIndex): ChangeSafety;
|
|
155
|
+
|
|
156
|
+
/** Whether a foreign key can be brought to its declaration in place. */
|
|
157
|
+
classifyForeignKeyChange(live: LiveForeignKey, declared: DeclaredForeignKey): ChangeSafety;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Whether `createTable` already carries the table's foreign keys, so the
|
|
161
|
+
* reconciler must not also plan an `addForeignKey` for a table it just made.
|
|
162
|
+
*
|
|
163
|
+
* REQUIRED, like every other member here, because the wrong answer is silent:
|
|
164
|
+
* a driver that omitted it would get name matching by default, and if its
|
|
165
|
+
* engine also emits keys inside `CREATE TABLE` it would get exactly the
|
|
166
|
+
* unrestartable application this member exists to prevent — with no compile
|
|
167
|
+
* error and no failing test. Stating an answer is the point.
|
|
168
|
+
*/
|
|
169
|
+
readonly foreignKeysInCreateTable: boolean;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Whether the engine reports a foreign key back under the name the
|
|
173
|
+
* declaration gave it.
|
|
174
|
+
*
|
|
175
|
+
* Separate from `foreignKeysInCreateTable` because they are separate facts and
|
|
176
|
+
* an engine can hold one without the other: MySQL emits keys inside `CREATE
|
|
177
|
+
* TABLE` and names them. Where this is false a declaration is matched to a
|
|
178
|
+
* live key by its columns, target and referential actions, since there is no
|
|
179
|
+
* name to match on and matching by one reads a table's own key as missing on
|
|
180
|
+
* every boot after the one that created it.
|
|
181
|
+
*/
|
|
182
|
+
readonly namesForeignKeys: boolean;
|
|
183
|
+
|
|
184
|
+
createTable(schema: string, table: DeclaredTable): string[];
|
|
185
|
+
addColumn(schema: string, table: string, column: DeclaredColumn): string[];
|
|
186
|
+
alterColumn(schema: string, table: string, live: LiveColumn, column: DeclaredColumn): string[];
|
|
187
|
+
copyColumn(schema: string, table: string, from: string, to: string): string[];
|
|
188
|
+
createIndex(schema: string, table: string, index: DeclaredIndex): string[];
|
|
189
|
+
dropIndex(schema: string, table: string, index: string): string[];
|
|
190
|
+
addForeignKey(schema: string, table: string, fk: DeclaredForeignKey): string[];
|
|
191
|
+
dropForeignKey(schema: string, table: string, name: string): string[];
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Whether this engine can reclaim an object of this kind at all.
|
|
195
|
+
*
|
|
196
|
+
* Asked BEFORE the drop is attempted, because a tombstone the engine cannot
|
|
197
|
+
* act on is otherwise permanent breakage rather than a one-off failure: it
|
|
198
|
+
* stays eligible, so every boot from then on fails in the same place, and the
|
|
199
|
+
* application never starts again. Reported instead, with the reason, and the
|
|
200
|
+
* tombstone is left standing.
|
|
201
|
+
*/
|
|
202
|
+
canReclaim(id: SchemaObjectId): ChangeSafety;
|
|
203
|
+
|
|
204
|
+
/** Reclamation. Separated from the rest because these are the only destructive statements. */
|
|
205
|
+
dropColumn(schema: string, table: string, column: string): string[];
|
|
206
|
+
dropTable(schema: string, table: string): string[];
|
|
207
|
+
}
|