@typokit/db-raw 0.1.4
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 +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +361 -0
- package/dist/index.js.map +1 -0
- package/package.json +29 -0
- package/src/index.test.ts +486 -0
- package/src/index.ts +478 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
// @typokit/db-raw — Raw SQL DDL Generation
|
|
2
|
+
import type {
|
|
3
|
+
SchemaTypeMap,
|
|
4
|
+
TypeMetadata,
|
|
5
|
+
GeneratedOutput,
|
|
6
|
+
MigrationDraft,
|
|
7
|
+
SchemaChange,
|
|
8
|
+
} from "@typokit/types";
|
|
9
|
+
import type { DatabaseAdapter, DatabaseState } from "@typokit/core";
|
|
10
|
+
|
|
11
|
+
export type RawSqlDialect = "postgresql" | "sqlite";
|
|
12
|
+
|
|
13
|
+
export interface RawSqlAdapterOptions {
|
|
14
|
+
dialect?: RawSqlDialect;
|
|
15
|
+
outputDir?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ─── Helpers ─────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function toSnakeCase(str: string): string {
|
|
21
|
+
return str
|
|
22
|
+
.replace(/([A-Z])/g, (_, letter, index) =>
|
|
23
|
+
index === 0 ? letter.toLowerCase() : `_${letter.toLowerCase()}`,
|
|
24
|
+
)
|
|
25
|
+
.replace(/__+/g, "_");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function pluralize(str: string): string {
|
|
29
|
+
if (str.endsWith("s")) return str;
|
|
30
|
+
if (str.endsWith("y") && !/[aeiou]y$/i.test(str))
|
|
31
|
+
return str.slice(0, -1) + "ies";
|
|
32
|
+
return str + "s";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toTableName(typeName: string, jsdoc?: Record<string, string>): string {
|
|
36
|
+
if (jsdoc?.table) return jsdoc.table;
|
|
37
|
+
return pluralize(toSnakeCase(typeName));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toColumnName(propName: string): string {
|
|
41
|
+
return toSnakeCase(propName);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Parse string union type like `"a" | "b" | "c"` into values */
|
|
45
|
+
export function parseUnionValues(typeStr: string): string[] | null {
|
|
46
|
+
const trimmed = typeStr.trim();
|
|
47
|
+
if (!trimmed.includes("|")) return null;
|
|
48
|
+
|
|
49
|
+
const parts = trimmed.split("|").map((p) => p.trim());
|
|
50
|
+
const values: string[] = [];
|
|
51
|
+
for (const part of parts) {
|
|
52
|
+
const match = /^"([^"]*)"$/.exec(part);
|
|
53
|
+
if (!match) return null;
|
|
54
|
+
values.push(match[1]);
|
|
55
|
+
}
|
|
56
|
+
return values.length > 0 ? values : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function toEnumName(typeName: string, propName: string): string {
|
|
60
|
+
return `${toSnakeCase(typeName)}_${toSnakeCase(propName)}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── PostgreSQL Column Mapping ──────────────────────────────
|
|
64
|
+
|
|
65
|
+
function mapPgColumnType(prop: {
|
|
66
|
+
type: string;
|
|
67
|
+
optional: boolean;
|
|
68
|
+
jsdoc?: Record<string, string>;
|
|
69
|
+
}): string {
|
|
70
|
+
const jsdoc = prop.jsdoc ?? {};
|
|
71
|
+
|
|
72
|
+
if (
|
|
73
|
+
prop.type === "string" &&
|
|
74
|
+
(jsdoc.id !== undefined || jsdoc.generated === "uuid")
|
|
75
|
+
) {
|
|
76
|
+
return "UUID";
|
|
77
|
+
}
|
|
78
|
+
if (prop.type === "string" && jsdoc.maxLength) {
|
|
79
|
+
return `VARCHAR(${jsdoc.maxLength})`;
|
|
80
|
+
}
|
|
81
|
+
if (prop.type === "string" && jsdoc.format === "email") {
|
|
82
|
+
return "VARCHAR(255)";
|
|
83
|
+
}
|
|
84
|
+
if (prop.type === "string") {
|
|
85
|
+
return "TEXT";
|
|
86
|
+
}
|
|
87
|
+
if (prop.type === "number") {
|
|
88
|
+
return "INTEGER";
|
|
89
|
+
}
|
|
90
|
+
if (prop.type === "bigint") {
|
|
91
|
+
return "BIGINT";
|
|
92
|
+
}
|
|
93
|
+
if (prop.type === "boolean") {
|
|
94
|
+
return "BOOLEAN";
|
|
95
|
+
}
|
|
96
|
+
if (prop.type === "Date") {
|
|
97
|
+
return "TIMESTAMPTZ";
|
|
98
|
+
}
|
|
99
|
+
// object, Record, unknown → JSONB
|
|
100
|
+
return "JSONB";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function mapSqliteColumnType(prop: {
|
|
104
|
+
type: string;
|
|
105
|
+
optional: boolean;
|
|
106
|
+
jsdoc?: Record<string, string>;
|
|
107
|
+
}): string {
|
|
108
|
+
if (prop.type === "number" || prop.type === "bigint") {
|
|
109
|
+
return "INTEGER";
|
|
110
|
+
}
|
|
111
|
+
if (prop.type === "boolean") {
|
|
112
|
+
return "INTEGER";
|
|
113
|
+
}
|
|
114
|
+
// string, Date, object, Record, enum unions → TEXT
|
|
115
|
+
return "TEXT";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── PostgreSQL DDL Generation ──────────────────────────────
|
|
119
|
+
|
|
120
|
+
interface EnumDef {
|
|
121
|
+
name: string;
|
|
122
|
+
values: string[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function generatePgDdl(
|
|
126
|
+
typeName: string,
|
|
127
|
+
meta: TypeMetadata,
|
|
128
|
+
): { ddl: string; enums: EnumDef[] } {
|
|
129
|
+
const tableName = toTableName(typeName, meta.jsdoc);
|
|
130
|
+
const enums: EnumDef[] = [];
|
|
131
|
+
const columns: string[] = [];
|
|
132
|
+
|
|
133
|
+
for (const [propName, prop] of Object.entries(meta.properties)) {
|
|
134
|
+
const col = toColumnName(propName);
|
|
135
|
+
const jsdoc = prop.jsdoc ?? {};
|
|
136
|
+
const parts: string[] = [col];
|
|
137
|
+
|
|
138
|
+
// Check for union → CREATE TYPE enum
|
|
139
|
+
const unionValues = parseUnionValues(prop.type);
|
|
140
|
+
if (unionValues) {
|
|
141
|
+
const enumName = toEnumName(typeName, propName);
|
|
142
|
+
enums.push({ name: enumName, values: unionValues });
|
|
143
|
+
parts.push(enumName);
|
|
144
|
+
} else {
|
|
145
|
+
parts.push(mapPgColumnType(prop));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// NOT NULL
|
|
149
|
+
if (!prop.optional) {
|
|
150
|
+
parts.push("NOT NULL");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// PRIMARY KEY
|
|
154
|
+
if (jsdoc.id !== undefined) {
|
|
155
|
+
parts.push("PRIMARY KEY");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// UNIQUE
|
|
159
|
+
if (jsdoc.unique !== undefined) {
|
|
160
|
+
parts.push("UNIQUE");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// DEFAULT
|
|
164
|
+
if (jsdoc.generated === "uuid") {
|
|
165
|
+
parts.push("DEFAULT gen_random_uuid()");
|
|
166
|
+
} else if (jsdoc.generated === "now" || jsdoc.onUpdate === "now") {
|
|
167
|
+
parts.push("DEFAULT now()");
|
|
168
|
+
} else if (jsdoc.default !== undefined) {
|
|
169
|
+
const defaultVal = jsdoc.default.replace(/^["']|["']$/g, "");
|
|
170
|
+
if (/^\d+$/.test(defaultVal)) {
|
|
171
|
+
parts.push(`DEFAULT ${defaultVal}`);
|
|
172
|
+
} else if (defaultVal === "true" || defaultVal === "false") {
|
|
173
|
+
parts.push(`DEFAULT ${defaultVal}`);
|
|
174
|
+
} else {
|
|
175
|
+
parts.push(`DEFAULT '${defaultVal}'`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
columns.push(" " + parts.join(" "));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let ddl = "";
|
|
183
|
+
|
|
184
|
+
// Enum type definitions
|
|
185
|
+
for (const e of enums) {
|
|
186
|
+
ddl += `CREATE TYPE ${e.name} AS ENUM (${e.values.map((v) => `'${v}'`).join(", ")});\n\n`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Table definition
|
|
190
|
+
ddl += `CREATE TABLE ${tableName} (\n`;
|
|
191
|
+
ddl += columns.join(",\n") + "\n";
|
|
192
|
+
ddl += ");";
|
|
193
|
+
|
|
194
|
+
return { ddl, enums };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─── SQLite DDL Generation ──────────────────────────────────
|
|
198
|
+
|
|
199
|
+
function generateSqliteDdl(typeName: string, meta: TypeMetadata): string {
|
|
200
|
+
const tableName = toTableName(typeName, meta.jsdoc);
|
|
201
|
+
const columns: string[] = [];
|
|
202
|
+
|
|
203
|
+
for (const [propName, prop] of Object.entries(meta.properties)) {
|
|
204
|
+
const col = toColumnName(propName);
|
|
205
|
+
const jsdoc = prop.jsdoc ?? {};
|
|
206
|
+
const parts: string[] = [col];
|
|
207
|
+
|
|
208
|
+
parts.push(mapSqliteColumnType(prop));
|
|
209
|
+
|
|
210
|
+
// NOT NULL
|
|
211
|
+
if (!prop.optional) {
|
|
212
|
+
parts.push("NOT NULL");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// PRIMARY KEY
|
|
216
|
+
if (jsdoc.id !== undefined) {
|
|
217
|
+
parts.push("PRIMARY KEY");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// UNIQUE
|
|
221
|
+
if (jsdoc.unique !== undefined) {
|
|
222
|
+
parts.push("UNIQUE");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// DEFAULT
|
|
226
|
+
if (jsdoc.default !== undefined) {
|
|
227
|
+
const defaultVal = jsdoc.default.replace(/^["']|["']$/g, "");
|
|
228
|
+
if (/^\d+$/.test(defaultVal)) {
|
|
229
|
+
parts.push(`DEFAULT ${defaultVal}`);
|
|
230
|
+
} else if (defaultVal === "true" || defaultVal === "false") {
|
|
231
|
+
parts.push(`DEFAULT ${defaultVal === "true" ? "1" : "0"}`);
|
|
232
|
+
} else {
|
|
233
|
+
parts.push(`DEFAULT '${defaultVal}'`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
columns.push(" " + parts.join(" "));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let ddl = `CREATE TABLE ${tableName} (\n`;
|
|
241
|
+
ddl += columns.join(",\n") + "\n";
|
|
242
|
+
ddl += ");";
|
|
243
|
+
|
|
244
|
+
return ddl;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ─── TypeScript Interface Generation ────────────────────────
|
|
248
|
+
|
|
249
|
+
function tsTypeFromProp(prop: { type: string; optional: boolean }): string {
|
|
250
|
+
const unionValues = parseUnionValues(prop.type);
|
|
251
|
+
if (unionValues) {
|
|
252
|
+
return unionValues.map((v) => `"${v}"`).join(" | ");
|
|
253
|
+
}
|
|
254
|
+
return prop.type;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function generateTypeScriptInterface(
|
|
258
|
+
typeName: string,
|
|
259
|
+
meta: TypeMetadata,
|
|
260
|
+
): string {
|
|
261
|
+
const lines: string[] = [];
|
|
262
|
+
lines.push(`export interface ${typeName} {`);
|
|
263
|
+
|
|
264
|
+
for (const [propName, prop] of Object.entries(meta.properties)) {
|
|
265
|
+
const tsType = tsTypeFromProp(prop);
|
|
266
|
+
const opt = prop.optional ? "?" : "";
|
|
267
|
+
lines.push(` ${propName}${opt}: ${tsType};`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
lines.push("}");
|
|
271
|
+
return lines.join("\n");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ─── Full File Generation ───────────────────────────────────
|
|
275
|
+
|
|
276
|
+
function generatePgFile(typeName: string, meta: TypeMetadata): GeneratedOutput {
|
|
277
|
+
const tableName = toTableName(typeName, meta.jsdoc);
|
|
278
|
+
const { ddl } = generatePgDdl(typeName, meta);
|
|
279
|
+
|
|
280
|
+
let content = `-- AUTO-GENERATED by @typokit/db-raw from ${typeName} type\n`;
|
|
281
|
+
content += `-- Do not edit manually — modify the source type instead\n\n`;
|
|
282
|
+
content += ddl + "\n";
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
filePath: `${tableName}.sql`,
|
|
286
|
+
content,
|
|
287
|
+
overwrite: true,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function generatePgTypesFile(types: SchemaTypeMap): GeneratedOutput {
|
|
292
|
+
let content = `// AUTO-GENERATED by @typokit/db-raw\n`;
|
|
293
|
+
content += `// Do not edit manually — modify the source types instead\n\n`;
|
|
294
|
+
|
|
295
|
+
const interfaces: string[] = [];
|
|
296
|
+
for (const [typeName, meta] of Object.entries(types)) {
|
|
297
|
+
interfaces.push(generateTypeScriptInterface(typeName, meta));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
content += interfaces.join("\n\n") + "\n";
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
filePath: "types.ts",
|
|
304
|
+
content,
|
|
305
|
+
overwrite: true,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function generateSqliteFile(
|
|
310
|
+
typeName: string,
|
|
311
|
+
meta: TypeMetadata,
|
|
312
|
+
): GeneratedOutput {
|
|
313
|
+
const tableName = toTableName(typeName, meta.jsdoc);
|
|
314
|
+
const ddl = generateSqliteDdl(typeName, meta);
|
|
315
|
+
|
|
316
|
+
let content = `-- AUTO-GENERATED by @typokit/db-raw from ${typeName} type\n`;
|
|
317
|
+
content += `-- Do not edit manually — modify the source type instead\n\n`;
|
|
318
|
+
content += ddl + "\n";
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
filePath: `${tableName}.sql`,
|
|
322
|
+
content,
|
|
323
|
+
overwrite: true,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ─── Diff Logic ─────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
function diffTypes(
|
|
330
|
+
types: SchemaTypeMap,
|
|
331
|
+
currentState: DatabaseState,
|
|
332
|
+
): SchemaChange[] {
|
|
333
|
+
const changes: SchemaChange[] = [];
|
|
334
|
+
|
|
335
|
+
for (const [typeName, meta] of Object.entries(types)) {
|
|
336
|
+
const tableName = toTableName(typeName, meta.jsdoc);
|
|
337
|
+
const existing = currentState.tables[tableName];
|
|
338
|
+
|
|
339
|
+
if (!existing) {
|
|
340
|
+
changes.push({ type: "add", entity: tableName });
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
for (const [propName, prop] of Object.entries(meta.properties)) {
|
|
345
|
+
const colName = toColumnName(propName);
|
|
346
|
+
const existingCol = existing.columns[colName];
|
|
347
|
+
|
|
348
|
+
if (!existingCol) {
|
|
349
|
+
changes.push({
|
|
350
|
+
type: "add",
|
|
351
|
+
entity: tableName,
|
|
352
|
+
field: colName,
|
|
353
|
+
details: { tsType: prop.type },
|
|
354
|
+
});
|
|
355
|
+
} else {
|
|
356
|
+
const nullable = prop.optional;
|
|
357
|
+
if (existingCol.nullable !== nullable) {
|
|
358
|
+
changes.push({
|
|
359
|
+
type: "modify",
|
|
360
|
+
entity: tableName,
|
|
361
|
+
field: colName,
|
|
362
|
+
details: {
|
|
363
|
+
nullableFrom: existingCol.nullable,
|
|
364
|
+
nullableTo: nullable,
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Detect removed columns
|
|
372
|
+
for (const colName of Object.keys(existing.columns)) {
|
|
373
|
+
const hasProp = Object.keys(meta.properties).some(
|
|
374
|
+
(p) => toColumnName(p) === colName,
|
|
375
|
+
);
|
|
376
|
+
if (!hasProp) {
|
|
377
|
+
changes.push({ type: "remove", entity: tableName, field: colName });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Detect removed tables
|
|
383
|
+
for (const tableName of Object.keys(currentState.tables)) {
|
|
384
|
+
const hasType = Object.entries(types).some(
|
|
385
|
+
([name, meta]) => toTableName(name, meta.jsdoc) === tableName,
|
|
386
|
+
);
|
|
387
|
+
if (!hasType) {
|
|
388
|
+
changes.push({ type: "remove", entity: tableName });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return changes;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function generateMigrationSql(
|
|
396
|
+
changes: SchemaChange[],
|
|
397
|
+
dialect: RawSqlDialect,
|
|
398
|
+
): string {
|
|
399
|
+
const lines: string[] = [];
|
|
400
|
+
|
|
401
|
+
for (const change of changes) {
|
|
402
|
+
if (change.type === "add" && !change.field) {
|
|
403
|
+
lines.push(`-- TODO: CREATE TABLE "${change.entity}" (define columns)`);
|
|
404
|
+
} else if (change.type === "add" && change.field) {
|
|
405
|
+
lines.push(
|
|
406
|
+
`ALTER TABLE "${change.entity}" ADD COLUMN "${change.field}" TEXT;`,
|
|
407
|
+
);
|
|
408
|
+
} else if (change.type === "remove" && !change.field) {
|
|
409
|
+
lines.push(`-- DESTRUCTIVE: requires review`);
|
|
410
|
+
lines.push(`DROP TABLE IF EXISTS "${change.entity}";`);
|
|
411
|
+
} else if (change.type === "remove" && change.field) {
|
|
412
|
+
lines.push(`-- DESTRUCTIVE: requires review`);
|
|
413
|
+
if (dialect === "postgresql") {
|
|
414
|
+
lines.push(
|
|
415
|
+
`ALTER TABLE "${change.entity}" DROP COLUMN "${change.field}";`,
|
|
416
|
+
);
|
|
417
|
+
} else {
|
|
418
|
+
lines.push(
|
|
419
|
+
`-- SQLite: cannot DROP COLUMN "${change.field}" from "${change.entity}" — recreate table`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
} else if (change.type === "modify") {
|
|
423
|
+
lines.push(
|
|
424
|
+
`-- TODO: ALTER TABLE "${change.entity}" modify column "${change.field}"`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return lines.join("\n");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ─── Adapter ────────────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
export class RawSqlDatabaseAdapter implements DatabaseAdapter {
|
|
435
|
+
private readonly dialect: RawSqlDialect;
|
|
436
|
+
private readonly outputDir: string;
|
|
437
|
+
|
|
438
|
+
constructor(options?: RawSqlAdapterOptions) {
|
|
439
|
+
this.dialect = options?.dialect ?? "postgresql";
|
|
440
|
+
this.outputDir = options?.outputDir ?? "sql";
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
generate(types: SchemaTypeMap): GeneratedOutput[] {
|
|
444
|
+
const outputs: GeneratedOutput[] = [];
|
|
445
|
+
const genFn =
|
|
446
|
+
this.dialect === "postgresql" ? generatePgFile : generateSqliteFile;
|
|
447
|
+
|
|
448
|
+
for (const [typeName, meta] of Object.entries(types)) {
|
|
449
|
+
const output = genFn(typeName, meta);
|
|
450
|
+
output.filePath = `${this.outputDir}/${output.filePath}`;
|
|
451
|
+
outputs.push(output);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Generate TypeScript interfaces file
|
|
455
|
+
const tsOutput = generatePgTypesFile(types);
|
|
456
|
+
tsOutput.filePath = `${this.outputDir}/${tsOutput.filePath}`;
|
|
457
|
+
outputs.push(tsOutput);
|
|
458
|
+
|
|
459
|
+
return outputs;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
diff(types: SchemaTypeMap, currentState: DatabaseState): MigrationDraft {
|
|
463
|
+
const changes = diffTypes(types, currentState);
|
|
464
|
+
const destructive = changes.some((c) => c.type === "remove");
|
|
465
|
+
const sql = generateMigrationSql(changes, this.dialect);
|
|
466
|
+
const timestamp = new Date()
|
|
467
|
+
.toISOString()
|
|
468
|
+
.replace(/[-:T]/g, "")
|
|
469
|
+
.slice(0, 14);
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
name: `${timestamp}_schema_update`,
|
|
473
|
+
sql,
|
|
474
|
+
destructive,
|
|
475
|
+
changes,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
}
|