@famgia/omnify-sql 0.0.2
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/README.md +201 -0
- package/dist/chunk-V7HEGSN4.js +1118 -0
- package/dist/chunk-V7HEGSN4.js.map +1 -0
- package/dist/chunk-YNWO77KO.cjs +1118 -0
- package/dist/chunk-YNWO77KO.cjs.map +1 -0
- package/dist/index.cjs +47 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +137 -0
- package/dist/index.d.ts +137 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.cjs +55 -0
- package/dist/plugin.cjs.map +1 -0
- package/dist/plugin.d.cts +68 -0
- package/dist/plugin.d.ts +68 -0
- package/dist/plugin.js +55 -0
- package/dist/plugin.js.map +1 -0
- package/dist/types-DkJkHX8d.d.cts +127 -0
- package/dist/types-DkJkHX8d.d.ts +127 -0
- package/package.json +66 -0
|
@@ -0,0 +1,1118 @@
|
|
|
1
|
+
// src/dialects/formatter.ts
|
|
2
|
+
function quoteIdentifier(name, dialect) {
|
|
3
|
+
switch (dialect) {
|
|
4
|
+
case "mysql":
|
|
5
|
+
return `\`${name}\``;
|
|
6
|
+
case "postgresql":
|
|
7
|
+
return `"${name}"`;
|
|
8
|
+
case "sqlite":
|
|
9
|
+
return `"${name}"`;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function quoteString(value) {
|
|
13
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
14
|
+
}
|
|
15
|
+
function formatColumn(column, dialect) {
|
|
16
|
+
const parts = [quoteIdentifier(column.name, dialect), column.type];
|
|
17
|
+
if (dialect === "mysql" && column.unsigned && !column.type.includes("UNSIGNED")) {
|
|
18
|
+
parts.push("UNSIGNED");
|
|
19
|
+
}
|
|
20
|
+
if (!column.nullable && !column.primaryKey) {
|
|
21
|
+
parts.push("NOT NULL");
|
|
22
|
+
} else if (column.nullable) {
|
|
23
|
+
parts.push("NULL");
|
|
24
|
+
}
|
|
25
|
+
if (column.autoIncrement) {
|
|
26
|
+
switch (dialect) {
|
|
27
|
+
case "mysql":
|
|
28
|
+
parts.push("AUTO_INCREMENT");
|
|
29
|
+
break;
|
|
30
|
+
case "sqlite":
|
|
31
|
+
break;
|
|
32
|
+
case "postgresql":
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (column.primaryKey) {
|
|
37
|
+
parts.push("PRIMARY KEY");
|
|
38
|
+
if (dialect === "sqlite" && column.autoIncrement) {
|
|
39
|
+
parts.push("AUTOINCREMENT");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (column.unique && !column.primaryKey) {
|
|
43
|
+
parts.push("UNIQUE");
|
|
44
|
+
}
|
|
45
|
+
if (column.defaultValue !== void 0) {
|
|
46
|
+
parts.push(`DEFAULT ${column.defaultValue}`);
|
|
47
|
+
}
|
|
48
|
+
if (column.comment && dialect === "mysql") {
|
|
49
|
+
parts.push(`COMMENT ${quoteString(column.comment)}`);
|
|
50
|
+
}
|
|
51
|
+
return parts.join(" ");
|
|
52
|
+
}
|
|
53
|
+
function formatForeignKey(fk, dialect) {
|
|
54
|
+
const localCols = fk.columns.map((c) => quoteIdentifier(c, dialect)).join(", ");
|
|
55
|
+
const refCols = fk.referencesColumns.map((c) => quoteIdentifier(c, dialect)).join(", ");
|
|
56
|
+
const refTable = quoteIdentifier(fk.referencesTable, dialect);
|
|
57
|
+
let sql = `CONSTRAINT ${quoteIdentifier(fk.name, dialect)} `;
|
|
58
|
+
sql += `FOREIGN KEY (${localCols}) `;
|
|
59
|
+
sql += `REFERENCES ${refTable} (${refCols})`;
|
|
60
|
+
if (fk.onDelete && fk.onDelete !== "NO ACTION") {
|
|
61
|
+
sql += ` ON DELETE ${fk.onDelete}`;
|
|
62
|
+
}
|
|
63
|
+
if (fk.onUpdate && fk.onUpdate !== "NO ACTION") {
|
|
64
|
+
sql += ` ON UPDATE ${fk.onUpdate}`;
|
|
65
|
+
}
|
|
66
|
+
return sql;
|
|
67
|
+
}
|
|
68
|
+
function formatIndex(index, tableName, dialect) {
|
|
69
|
+
const cols = index.columns.map((c) => quoteIdentifier(c, dialect)).join(", ");
|
|
70
|
+
const table = quoteIdentifier(tableName, dialect);
|
|
71
|
+
const name = quoteIdentifier(index.name, dialect);
|
|
72
|
+
if (index.type === "fulltext") {
|
|
73
|
+
switch (dialect) {
|
|
74
|
+
case "mysql":
|
|
75
|
+
return `CREATE FULLTEXT INDEX ${name} ON ${table} (${cols});`;
|
|
76
|
+
case "postgresql":
|
|
77
|
+
const tsvectorCols = index.columns.map((c) => `to_tsvector('english', ${quoteIdentifier(c, dialect)})`).join(" || ");
|
|
78
|
+
return `CREATE INDEX ${name} ON ${table} USING GIN (${tsvectorCols});`;
|
|
79
|
+
case "sqlite":
|
|
80
|
+
return `CREATE INDEX ${name} ON ${table} (${cols});`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (index.type === "spatial") {
|
|
84
|
+
switch (dialect) {
|
|
85
|
+
case "mysql":
|
|
86
|
+
return `CREATE SPATIAL INDEX ${name} ON ${table} (${cols});`;
|
|
87
|
+
case "postgresql":
|
|
88
|
+
return `CREATE INDEX ${name} ON ${table} USING GIST (${cols});`;
|
|
89
|
+
case "sqlite":
|
|
90
|
+
return `CREATE INDEX ${name} ON ${table} (${cols});`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (index.type === "gin" && dialect === "postgresql") {
|
|
94
|
+
return `CREATE INDEX ${name} ON ${table} USING GIN (${cols});`;
|
|
95
|
+
}
|
|
96
|
+
if (index.type === "gist" && dialect === "postgresql") {
|
|
97
|
+
return `CREATE INDEX ${name} ON ${table} USING GIST (${cols});`;
|
|
98
|
+
}
|
|
99
|
+
if (index.type === "hash") {
|
|
100
|
+
switch (dialect) {
|
|
101
|
+
case "mysql":
|
|
102
|
+
return `CREATE INDEX ${name} ON ${table} (${cols}) USING HASH;`;
|
|
103
|
+
case "postgresql":
|
|
104
|
+
return `CREATE INDEX ${name} ON ${table} USING HASH (${cols});`;
|
|
105
|
+
case "sqlite":
|
|
106
|
+
return `CREATE INDEX ${name} ON ${table} (${cols});`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const indexType = index.unique ? "UNIQUE INDEX" : "INDEX";
|
|
110
|
+
return `CREATE ${indexType} ${name} ON ${table} (${cols});`;
|
|
111
|
+
}
|
|
112
|
+
function formatCreateTable(table, dialect, options) {
|
|
113
|
+
const tableName = quoteIdentifier(table.name, dialect);
|
|
114
|
+
const ifNotExists = options?.ifNotExists ? "IF NOT EXISTS " : "";
|
|
115
|
+
const lines = [];
|
|
116
|
+
for (const column of table.columns) {
|
|
117
|
+
lines.push(` ${formatColumn(column, dialect)}`);
|
|
118
|
+
}
|
|
119
|
+
for (const fk of table.foreignKeys) {
|
|
120
|
+
lines.push(` ${formatForeignKey(fk, dialect)}`);
|
|
121
|
+
}
|
|
122
|
+
let sql = `CREATE TABLE ${ifNotExists}${tableName} (
|
|
123
|
+
`;
|
|
124
|
+
sql += lines.join(",\n");
|
|
125
|
+
sql += "\n)";
|
|
126
|
+
if (dialect === "mysql") {
|
|
127
|
+
sql += " ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
|
|
128
|
+
}
|
|
129
|
+
sql += ";";
|
|
130
|
+
if (table.comment && dialect === "postgresql") {
|
|
131
|
+
sql += `
|
|
132
|
+
|
|
133
|
+
COMMENT ON TABLE ${tableName} IS ${quoteString(table.comment)};`;
|
|
134
|
+
}
|
|
135
|
+
return sql;
|
|
136
|
+
}
|
|
137
|
+
function formatDropTable(tableName, dialect, options) {
|
|
138
|
+
const table = quoteIdentifier(tableName, dialect);
|
|
139
|
+
const ifExists = options?.ifExists ? "IF EXISTS " : "";
|
|
140
|
+
const cascade = options?.cascade && dialect === "postgresql" ? " CASCADE" : "";
|
|
141
|
+
return `DROP TABLE ${ifExists}${table}${cascade};`;
|
|
142
|
+
}
|
|
143
|
+
function formatIndexes(table, dialect) {
|
|
144
|
+
return table.indexes.map((index) => formatIndex(index, table.name, dialect));
|
|
145
|
+
}
|
|
146
|
+
function formatColumnComments(table, dialect) {
|
|
147
|
+
if (dialect !== "postgresql") {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
const statements = [];
|
|
151
|
+
const tableName = quoteIdentifier(table.name, dialect);
|
|
152
|
+
for (const column of table.columns) {
|
|
153
|
+
if (column.comment) {
|
|
154
|
+
const colName = quoteIdentifier(column.name, dialect);
|
|
155
|
+
statements.push(
|
|
156
|
+
`COMMENT ON COLUMN ${tableName}.${colName} IS ${quoteString(column.comment)};`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return statements;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/dialects/types.ts
|
|
164
|
+
var MYSQL_TYPES = {
|
|
165
|
+
String: { type: "VARCHAR", hasLength: true, defaultLength: 255 },
|
|
166
|
+
Int: { type: "INT" },
|
|
167
|
+
BigInt: { type: "BIGINT" },
|
|
168
|
+
Float: { type: "DOUBLE" },
|
|
169
|
+
Decimal: { type: "DECIMAL", hasPrecision: true },
|
|
170
|
+
Boolean: { type: "TINYINT(1)" },
|
|
171
|
+
Text: { type: "TEXT" },
|
|
172
|
+
LongText: { type: "LONGTEXT" },
|
|
173
|
+
Date: { type: "DATE" },
|
|
174
|
+
Time: { type: "TIME" },
|
|
175
|
+
Timestamp: { type: "TIMESTAMP" },
|
|
176
|
+
Json: { type: "JSON" },
|
|
177
|
+
Email: { type: "VARCHAR", hasLength: true, defaultLength: 255 },
|
|
178
|
+
Password: { type: "VARCHAR", hasLength: true, defaultLength: 255 },
|
|
179
|
+
File: { type: "VARCHAR", hasLength: true, defaultLength: 255 },
|
|
180
|
+
MultiFile: { type: "JSON" },
|
|
181
|
+
Uuid: { type: "CHAR(36)" },
|
|
182
|
+
Select: { type: "VARCHAR", hasLength: true, defaultLength: 255 },
|
|
183
|
+
Lookup: { type: "BIGINT UNSIGNED" },
|
|
184
|
+
// Spatial types
|
|
185
|
+
Point: { type: "POINT" },
|
|
186
|
+
Coordinates: { type: "DECIMAL(10, 8)" }
|
|
187
|
+
// For latitude column (longitude uses DECIMAL(11, 8))
|
|
188
|
+
};
|
|
189
|
+
var POSTGRESQL_TYPES = {
|
|
190
|
+
String: { type: "VARCHAR", hasLength: true, defaultLength: 255 },
|
|
191
|
+
Int: { type: "INTEGER" },
|
|
192
|
+
BigInt: { type: "BIGINT" },
|
|
193
|
+
Float: { type: "DOUBLE PRECISION" },
|
|
194
|
+
Decimal: { type: "DECIMAL", hasPrecision: true },
|
|
195
|
+
Boolean: { type: "BOOLEAN" },
|
|
196
|
+
Text: { type: "TEXT" },
|
|
197
|
+
LongText: { type: "TEXT" },
|
|
198
|
+
Date: { type: "DATE" },
|
|
199
|
+
Time: { type: "TIME" },
|
|
200
|
+
Timestamp: { type: "TIMESTAMP" },
|
|
201
|
+
Json: { type: "JSONB" },
|
|
202
|
+
Email: { type: "VARCHAR", hasLength: true, defaultLength: 255 },
|
|
203
|
+
Password: { type: "VARCHAR", hasLength: true, defaultLength: 255 },
|
|
204
|
+
File: { type: "VARCHAR", hasLength: true, defaultLength: 255 },
|
|
205
|
+
MultiFile: { type: "JSONB" },
|
|
206
|
+
Uuid: { type: "UUID" },
|
|
207
|
+
Select: { type: "VARCHAR", hasLength: true, defaultLength: 255 },
|
|
208
|
+
Lookup: { type: "BIGINT" },
|
|
209
|
+
// Spatial types (requires PostGIS extension)
|
|
210
|
+
Point: { type: "geometry(Point, 4326)" },
|
|
211
|
+
Coordinates: { type: "DECIMAL(10, 8)" }
|
|
212
|
+
// For latitude column
|
|
213
|
+
};
|
|
214
|
+
var SQLITE_TYPES = {
|
|
215
|
+
String: { type: "TEXT" },
|
|
216
|
+
Int: { type: "INTEGER" },
|
|
217
|
+
BigInt: { type: "INTEGER" },
|
|
218
|
+
Float: { type: "REAL" },
|
|
219
|
+
Decimal: { type: "REAL" },
|
|
220
|
+
Boolean: { type: "INTEGER" },
|
|
221
|
+
Text: { type: "TEXT" },
|
|
222
|
+
LongText: { type: "TEXT" },
|
|
223
|
+
Date: { type: "TEXT" },
|
|
224
|
+
Time: { type: "TEXT" },
|
|
225
|
+
Timestamp: { type: "TEXT" },
|
|
226
|
+
Json: { type: "TEXT" },
|
|
227
|
+
Email: { type: "TEXT" },
|
|
228
|
+
Password: { type: "TEXT" },
|
|
229
|
+
File: { type: "TEXT" },
|
|
230
|
+
MultiFile: { type: "TEXT" },
|
|
231
|
+
Uuid: { type: "TEXT" },
|
|
232
|
+
Select: { type: "TEXT" },
|
|
233
|
+
Lookup: { type: "INTEGER" },
|
|
234
|
+
// Spatial types (no native support, use TEXT for JSON or REAL for lat/lon)
|
|
235
|
+
Point: { type: "TEXT" },
|
|
236
|
+
// JSON representation: {"lat": 0, "lon": 0}
|
|
237
|
+
Coordinates: { type: "REAL" }
|
|
238
|
+
// For latitude/longitude columns
|
|
239
|
+
};
|
|
240
|
+
var DIALECT_TYPES = {
|
|
241
|
+
mysql: MYSQL_TYPES,
|
|
242
|
+
postgresql: POSTGRESQL_TYPES,
|
|
243
|
+
sqlite: SQLITE_TYPES
|
|
244
|
+
};
|
|
245
|
+
function getSqlType(omnifyType, dialect, options) {
|
|
246
|
+
const mapping = DIALECT_TYPES[dialect][omnifyType];
|
|
247
|
+
if (!mapping) {
|
|
248
|
+
return dialect === "sqlite" ? "TEXT" : "VARCHAR(255)";
|
|
249
|
+
}
|
|
250
|
+
let type = mapping.type;
|
|
251
|
+
if (mapping.hasLength) {
|
|
252
|
+
const length = options?.length ?? mapping.defaultLength ?? 255;
|
|
253
|
+
type = `${type}(${length})`;
|
|
254
|
+
}
|
|
255
|
+
if (mapping.hasPrecision && options?.precision) {
|
|
256
|
+
const scale = options.scale ?? 2;
|
|
257
|
+
type = `${type}(${options.precision}, ${scale})`;
|
|
258
|
+
}
|
|
259
|
+
return type;
|
|
260
|
+
}
|
|
261
|
+
function getPrimaryKeyType(idType, dialect) {
|
|
262
|
+
switch (dialect) {
|
|
263
|
+
case "mysql":
|
|
264
|
+
switch (idType) {
|
|
265
|
+
case "Int":
|
|
266
|
+
return { type: "INT UNSIGNED", autoIncrement: true };
|
|
267
|
+
case "BigInt":
|
|
268
|
+
return { type: "BIGINT UNSIGNED", autoIncrement: true };
|
|
269
|
+
case "Uuid":
|
|
270
|
+
return { type: "CHAR(36)", autoIncrement: false };
|
|
271
|
+
case "String":
|
|
272
|
+
return { type: "VARCHAR(255)", autoIncrement: false };
|
|
273
|
+
}
|
|
274
|
+
break;
|
|
275
|
+
case "postgresql":
|
|
276
|
+
switch (idType) {
|
|
277
|
+
case "Int":
|
|
278
|
+
return { type: "SERIAL", autoIncrement: false };
|
|
279
|
+
// SERIAL handles auto-increment
|
|
280
|
+
case "BigInt":
|
|
281
|
+
return { type: "BIGSERIAL", autoIncrement: false };
|
|
282
|
+
case "Uuid":
|
|
283
|
+
return { type: "UUID", autoIncrement: false };
|
|
284
|
+
case "String":
|
|
285
|
+
return { type: "VARCHAR(255)", autoIncrement: false };
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
case "sqlite":
|
|
289
|
+
switch (idType) {
|
|
290
|
+
case "Int":
|
|
291
|
+
case "BigInt":
|
|
292
|
+
return { type: "INTEGER", autoIncrement: true };
|
|
293
|
+
case "Uuid":
|
|
294
|
+
case "String":
|
|
295
|
+
return { type: "TEXT", autoIncrement: false };
|
|
296
|
+
}
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
return { type: "BIGINT", autoIncrement: true };
|
|
300
|
+
}
|
|
301
|
+
function getForeignKeyType(referencedIdType, dialect) {
|
|
302
|
+
switch (dialect) {
|
|
303
|
+
case "mysql":
|
|
304
|
+
switch (referencedIdType) {
|
|
305
|
+
case "Int":
|
|
306
|
+
return "INT UNSIGNED";
|
|
307
|
+
case "BigInt":
|
|
308
|
+
return "BIGINT UNSIGNED";
|
|
309
|
+
case "Uuid":
|
|
310
|
+
return "CHAR(36)";
|
|
311
|
+
case "String":
|
|
312
|
+
return "VARCHAR(255)";
|
|
313
|
+
}
|
|
314
|
+
break;
|
|
315
|
+
case "postgresql":
|
|
316
|
+
switch (referencedIdType) {
|
|
317
|
+
case "Int":
|
|
318
|
+
return "INTEGER";
|
|
319
|
+
case "BigInt":
|
|
320
|
+
return "BIGINT";
|
|
321
|
+
case "Uuid":
|
|
322
|
+
return "UUID";
|
|
323
|
+
case "String":
|
|
324
|
+
return "VARCHAR(255)";
|
|
325
|
+
}
|
|
326
|
+
break;
|
|
327
|
+
case "sqlite":
|
|
328
|
+
switch (referencedIdType) {
|
|
329
|
+
case "Int":
|
|
330
|
+
case "BigInt":
|
|
331
|
+
return "INTEGER";
|
|
332
|
+
case "Uuid":
|
|
333
|
+
case "String":
|
|
334
|
+
return "TEXT";
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
return "BIGINT";
|
|
339
|
+
}
|
|
340
|
+
function getEnumType(values, dialect, enumName) {
|
|
341
|
+
switch (dialect) {
|
|
342
|
+
case "mysql":
|
|
343
|
+
const enumValues = values.map((v) => `'${v}'`).join(", ");
|
|
344
|
+
return { type: `ENUM(${enumValues})` };
|
|
345
|
+
case "postgresql":
|
|
346
|
+
const typeName = enumName ?? "enum_type";
|
|
347
|
+
const pgValues = values.map((v) => `'${v}'`).join(", ");
|
|
348
|
+
return {
|
|
349
|
+
type: typeName,
|
|
350
|
+
preStatement: `CREATE TYPE ${typeName} AS ENUM (${pgValues});`
|
|
351
|
+
};
|
|
352
|
+
case "sqlite":
|
|
353
|
+
return { type: "TEXT" };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/migration/schema-builder.ts
|
|
358
|
+
function toSnakeCase(str) {
|
|
359
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`).replace(/^_/, "");
|
|
360
|
+
}
|
|
361
|
+
function toTableName(schemaName) {
|
|
362
|
+
const snake = toSnakeCase(schemaName);
|
|
363
|
+
if (snake.endsWith("y") && !["ay", "ey", "oy", "uy"].some((v) => snake.endsWith(v))) {
|
|
364
|
+
return snake.slice(0, -1) + "ies";
|
|
365
|
+
}
|
|
366
|
+
if (snake.endsWith("s") || snake.endsWith("x") || snake.endsWith("ch") || snake.endsWith("sh")) {
|
|
367
|
+
return snake + "es";
|
|
368
|
+
}
|
|
369
|
+
return snake + "s";
|
|
370
|
+
}
|
|
371
|
+
function toColumnName(propertyName) {
|
|
372
|
+
return toSnakeCase(propertyName);
|
|
373
|
+
}
|
|
374
|
+
function generatePrimaryKey(idType, dialect) {
|
|
375
|
+
const pkInfo = getPrimaryKeyType(idType, dialect);
|
|
376
|
+
return {
|
|
377
|
+
name: "id",
|
|
378
|
+
type: pkInfo.type,
|
|
379
|
+
nullable: false,
|
|
380
|
+
primaryKey: true,
|
|
381
|
+
autoIncrement: pkInfo.autoIncrement,
|
|
382
|
+
unique: false,
|
|
383
|
+
unsigned: false
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function generateTimestampColumns(dialect) {
|
|
387
|
+
const timestampType = dialect === "sqlite" ? "TEXT" : "TIMESTAMP";
|
|
388
|
+
return [
|
|
389
|
+
{
|
|
390
|
+
name: "created_at",
|
|
391
|
+
type: timestampType,
|
|
392
|
+
nullable: true,
|
|
393
|
+
primaryKey: false,
|
|
394
|
+
autoIncrement: false,
|
|
395
|
+
unique: false,
|
|
396
|
+
unsigned: false
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
name: "updated_at",
|
|
400
|
+
type: timestampType,
|
|
401
|
+
nullable: true,
|
|
402
|
+
primaryKey: false,
|
|
403
|
+
autoIncrement: false,
|
|
404
|
+
unique: false,
|
|
405
|
+
unsigned: false
|
|
406
|
+
}
|
|
407
|
+
];
|
|
408
|
+
}
|
|
409
|
+
function generateSoftDeleteColumn(dialect) {
|
|
410
|
+
const timestampType = dialect === "sqlite" ? "TEXT" : "TIMESTAMP";
|
|
411
|
+
return {
|
|
412
|
+
name: "deleted_at",
|
|
413
|
+
type: timestampType,
|
|
414
|
+
nullable: true,
|
|
415
|
+
primaryKey: false,
|
|
416
|
+
autoIncrement: false,
|
|
417
|
+
unique: false,
|
|
418
|
+
unsigned: false
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function generateCoordinatesColumns(name, property, dialect) {
|
|
422
|
+
const baseName = toColumnName(name);
|
|
423
|
+
const nullable = property.nullable ?? false;
|
|
424
|
+
const latType = dialect === "sqlite" ? "REAL" : "DECIMAL(10, 8)";
|
|
425
|
+
const lonType = dialect === "sqlite" ? "REAL" : "DECIMAL(11, 8)";
|
|
426
|
+
return [
|
|
427
|
+
{
|
|
428
|
+
name: `${baseName}_latitude`,
|
|
429
|
+
type: latType,
|
|
430
|
+
nullable,
|
|
431
|
+
primaryKey: false,
|
|
432
|
+
autoIncrement: false,
|
|
433
|
+
unique: false,
|
|
434
|
+
unsigned: false,
|
|
435
|
+
comment: `Latitude for ${name}`
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: `${baseName}_longitude`,
|
|
439
|
+
type: lonType,
|
|
440
|
+
nullable,
|
|
441
|
+
primaryKey: false,
|
|
442
|
+
autoIncrement: false,
|
|
443
|
+
unique: false,
|
|
444
|
+
unsigned: false,
|
|
445
|
+
comment: `Longitude for ${name}`
|
|
446
|
+
}
|
|
447
|
+
];
|
|
448
|
+
}
|
|
449
|
+
function propertyToColumn(name, property, dialect, _allSchemas) {
|
|
450
|
+
if (property.type === "Association") {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
if (property.type === "Coordinates") {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
const columnName = toColumnName(name);
|
|
457
|
+
const baseProp = property;
|
|
458
|
+
if (property.type === "Enum") {
|
|
459
|
+
if (Array.isArray(baseProp.enum)) {
|
|
460
|
+
const enumInfo = getEnumType(baseProp.enum, dialect, `${columnName}_enum`);
|
|
461
|
+
return {
|
|
462
|
+
name: columnName,
|
|
463
|
+
type: enumInfo.type,
|
|
464
|
+
nullable: baseProp.nullable ?? false,
|
|
465
|
+
primaryKey: false,
|
|
466
|
+
autoIncrement: false,
|
|
467
|
+
unique: baseProp.unique ?? false,
|
|
468
|
+
unsigned: false,
|
|
469
|
+
defaultValue: baseProp.default !== void 0 ? `'${baseProp.default}'` : void 0,
|
|
470
|
+
comment: baseProp.displayName
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (property.type === "Select" && baseProp.options) {
|
|
475
|
+
const enumInfo = getEnumType(baseProp.options, dialect, `${columnName}_enum`);
|
|
476
|
+
return {
|
|
477
|
+
name: columnName,
|
|
478
|
+
type: enumInfo.type,
|
|
479
|
+
nullable: baseProp.nullable ?? false,
|
|
480
|
+
primaryKey: false,
|
|
481
|
+
autoIncrement: false,
|
|
482
|
+
unique: baseProp.unique ?? false,
|
|
483
|
+
unsigned: false,
|
|
484
|
+
defaultValue: baseProp.default !== void 0 ? `'${baseProp.default}'` : void 0,
|
|
485
|
+
comment: baseProp.displayName
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
const sqlType = getSqlType(property.type, dialect, {
|
|
489
|
+
length: baseProp.length,
|
|
490
|
+
precision: baseProp.precision,
|
|
491
|
+
scale: baseProp.scale
|
|
492
|
+
});
|
|
493
|
+
let defaultValue;
|
|
494
|
+
if (baseProp.default !== void 0) {
|
|
495
|
+
if (typeof baseProp.default === "string") {
|
|
496
|
+
defaultValue = `'${baseProp.default}'`;
|
|
497
|
+
} else if (typeof baseProp.default === "boolean") {
|
|
498
|
+
defaultValue = dialect === "postgresql" ? baseProp.default ? "TRUE" : "FALSE" : baseProp.default ? "1" : "0";
|
|
499
|
+
} else {
|
|
500
|
+
defaultValue = String(baseProp.default);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
name: columnName,
|
|
505
|
+
type: sqlType,
|
|
506
|
+
nullable: baseProp.nullable ?? false,
|
|
507
|
+
primaryKey: false,
|
|
508
|
+
autoIncrement: false,
|
|
509
|
+
unique: baseProp.unique ?? false,
|
|
510
|
+
unsigned: false,
|
|
511
|
+
defaultValue,
|
|
512
|
+
comment: baseProp.displayName
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function generateForeignKey(name, property, schema, dialect, allSchemas) {
|
|
516
|
+
if (property.type !== "Association") {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
const assocProp = property;
|
|
520
|
+
if (!["ManyToOne", "OneToOne"].includes(assocProp.relation ?? "")) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
const targetName = assocProp.target;
|
|
524
|
+
if (!targetName) {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
const targetSchema = allSchemas[targetName];
|
|
528
|
+
const targetIdType = targetSchema?.options?.idType ?? "BigInt";
|
|
529
|
+
const columnName = `${toColumnName(name)}_id`;
|
|
530
|
+
const fkType = getForeignKeyType(targetIdType, dialect);
|
|
531
|
+
const constraintName = `fk_${toTableName(schema.name)}_${columnName}`;
|
|
532
|
+
const column = {
|
|
533
|
+
name: columnName,
|
|
534
|
+
type: fkType,
|
|
535
|
+
nullable: property.nullable ?? false,
|
|
536
|
+
primaryKey: false,
|
|
537
|
+
autoIncrement: false,
|
|
538
|
+
unique: false,
|
|
539
|
+
unsigned: dialect === "mysql" && ["Int", "BigInt"].includes(targetIdType)
|
|
540
|
+
};
|
|
541
|
+
const foreignKey = {
|
|
542
|
+
name: constraintName,
|
|
543
|
+
columns: [columnName],
|
|
544
|
+
referencesTable: toTableName(targetName),
|
|
545
|
+
referencesColumns: ["id"],
|
|
546
|
+
onDelete: assocProp.onDelete ?? "CASCADE",
|
|
547
|
+
onUpdate: assocProp.onUpdate ?? "CASCADE"
|
|
548
|
+
};
|
|
549
|
+
return { column, foreignKey };
|
|
550
|
+
}
|
|
551
|
+
function generatePolymorphicColumns(name, property, dialect, allSchemas) {
|
|
552
|
+
if (property.type !== "Association") {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
const assocProp = property;
|
|
556
|
+
if (assocProp.relation !== "MorphTo" || !assocProp.targets?.length) {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
const baseName = toColumnName(name);
|
|
560
|
+
let useUuid = false;
|
|
561
|
+
for (const targetName of assocProp.targets) {
|
|
562
|
+
const targetSchema = allSchemas[targetName];
|
|
563
|
+
if (targetSchema?.options?.idType === "Uuid") {
|
|
564
|
+
useUuid = true;
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const enumInfo = getEnumType(assocProp.targets, dialect, `${baseName}_type_enum`);
|
|
569
|
+
const typeColumn = {
|
|
570
|
+
name: `${baseName}_type`,
|
|
571
|
+
type: enumInfo.type,
|
|
572
|
+
nullable: true,
|
|
573
|
+
primaryKey: false,
|
|
574
|
+
autoIncrement: false,
|
|
575
|
+
unique: false,
|
|
576
|
+
unsigned: false
|
|
577
|
+
};
|
|
578
|
+
const idType = useUuid ? dialect === "mysql" ? "CHAR(36)" : dialect === "postgresql" ? "UUID" : "TEXT" : dialect === "mysql" ? "BIGINT UNSIGNED" : dialect === "postgresql" ? "BIGINT" : "INTEGER";
|
|
579
|
+
const idColumn = {
|
|
580
|
+
name: `${baseName}_id`,
|
|
581
|
+
type: idType,
|
|
582
|
+
nullable: true,
|
|
583
|
+
primaryKey: false,
|
|
584
|
+
autoIncrement: false,
|
|
585
|
+
unique: false,
|
|
586
|
+
unsigned: dialect === "mysql" && !useUuid
|
|
587
|
+
};
|
|
588
|
+
const index = {
|
|
589
|
+
name: `idx_${baseName}_type_id`,
|
|
590
|
+
columns: [`${baseName}_type`, `${baseName}_id`],
|
|
591
|
+
unique: false
|
|
592
|
+
};
|
|
593
|
+
return {
|
|
594
|
+
columns: [typeColumn, idColumn],
|
|
595
|
+
index
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
function schemaToTable(schema, allSchemas, options) {
|
|
599
|
+
const columns = [];
|
|
600
|
+
const foreignKeys = [];
|
|
601
|
+
const indexes = [];
|
|
602
|
+
const dialect = options.dialect;
|
|
603
|
+
if (schema.options?.id !== false) {
|
|
604
|
+
const idType = schema.options?.idType ?? "BigInt";
|
|
605
|
+
columns.push(generatePrimaryKey(idType, dialect));
|
|
606
|
+
}
|
|
607
|
+
if (schema.properties) {
|
|
608
|
+
for (const [propName, property] of Object.entries(schema.properties)) {
|
|
609
|
+
const column = propertyToColumn(propName, property, dialect, allSchemas);
|
|
610
|
+
if (column) {
|
|
611
|
+
columns.push(column);
|
|
612
|
+
if (column.unique) {
|
|
613
|
+
indexes.push({
|
|
614
|
+
name: `idx_${toTableName(schema.name)}_${column.name}_unique`,
|
|
615
|
+
columns: [column.name],
|
|
616
|
+
unique: true
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
const fkResult = generateForeignKey(propName, property, schema, dialect, allSchemas);
|
|
622
|
+
if (fkResult) {
|
|
623
|
+
columns.push(fkResult.column);
|
|
624
|
+
foreignKeys.push(fkResult.foreignKey);
|
|
625
|
+
indexes.push({
|
|
626
|
+
name: `idx_${toTableName(schema.name)}_${fkResult.column.name}`,
|
|
627
|
+
columns: [fkResult.column.name],
|
|
628
|
+
unique: false
|
|
629
|
+
});
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
const morphResult = generatePolymorphicColumns(propName, property, dialect, allSchemas);
|
|
633
|
+
if (morphResult) {
|
|
634
|
+
columns.push(...morphResult.columns);
|
|
635
|
+
indexes.push(morphResult.index);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (property.type === "Coordinates") {
|
|
639
|
+
const coordColumns = generateCoordinatesColumns(propName, property, dialect);
|
|
640
|
+
columns.push(...coordColumns);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (schema.options?.timestamps !== false) {
|
|
645
|
+
columns.push(...generateTimestampColumns(dialect));
|
|
646
|
+
}
|
|
647
|
+
if (schema.options?.softDelete) {
|
|
648
|
+
columns.push(generateSoftDeleteColumn(dialect));
|
|
649
|
+
}
|
|
650
|
+
if (schema.options?.indexes) {
|
|
651
|
+
for (const indexDef of schema.options.indexes) {
|
|
652
|
+
const indexName = indexDef.name ?? `idx_${toTableName(schema.name)}_${indexDef.columns.map((c) => toColumnName(c)).join("_")}`;
|
|
653
|
+
indexes.push({
|
|
654
|
+
name: indexName,
|
|
655
|
+
columns: indexDef.columns.map((c) => toColumnName(c)),
|
|
656
|
+
unique: indexDef.unique ?? false,
|
|
657
|
+
type: indexDef.type
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return {
|
|
662
|
+
name: toTableName(schema.name),
|
|
663
|
+
columns,
|
|
664
|
+
foreignKeys,
|
|
665
|
+
indexes,
|
|
666
|
+
comment: schema.displayName
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
function generatePivotTable(sourceSchema, _propertyName, property, allSchemas, options) {
|
|
670
|
+
if (property.type !== "Association") {
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
const assocProp = property;
|
|
674
|
+
if (assocProp.relation !== "ManyToMany" || !assocProp.target) {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
const targetSchema = allSchemas[assocProp.target];
|
|
678
|
+
if (!targetSchema) {
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
const dialect = options.dialect;
|
|
682
|
+
const sourceTable = toTableName(sourceSchema.name);
|
|
683
|
+
const targetTable = toTableName(assocProp.target);
|
|
684
|
+
const tables = [sourceTable, targetTable].sort();
|
|
685
|
+
const pivotName = assocProp.joinTable ?? `${tables[0]}_${tables[1]}`;
|
|
686
|
+
const sourceIdType = sourceSchema.options?.idType ?? "BigInt";
|
|
687
|
+
const targetIdType = targetSchema.options?.idType ?? "BigInt";
|
|
688
|
+
const sourceColName = `${toSnakeCase(sourceSchema.name)}_id`;
|
|
689
|
+
const targetColName = `${toSnakeCase(assocProp.target)}_id`;
|
|
690
|
+
const columns = [
|
|
691
|
+
{
|
|
692
|
+
name: sourceColName,
|
|
693
|
+
type: getForeignKeyType(sourceIdType, dialect),
|
|
694
|
+
nullable: false,
|
|
695
|
+
primaryKey: false,
|
|
696
|
+
autoIncrement: false,
|
|
697
|
+
unique: false,
|
|
698
|
+
unsigned: dialect === "mysql" && ["Int", "BigInt"].includes(sourceIdType)
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
name: targetColName,
|
|
702
|
+
type: getForeignKeyType(targetIdType, dialect),
|
|
703
|
+
nullable: false,
|
|
704
|
+
primaryKey: false,
|
|
705
|
+
autoIncrement: false,
|
|
706
|
+
unique: false,
|
|
707
|
+
unsigned: dialect === "mysql" && ["Int", "BigInt"].includes(targetIdType)
|
|
708
|
+
}
|
|
709
|
+
];
|
|
710
|
+
const foreignKeys = [
|
|
711
|
+
{
|
|
712
|
+
name: `fk_${pivotName}_${sourceColName}`,
|
|
713
|
+
columns: [sourceColName],
|
|
714
|
+
referencesTable: sourceTable,
|
|
715
|
+
referencesColumns: ["id"],
|
|
716
|
+
onDelete: assocProp.onDelete ?? "CASCADE",
|
|
717
|
+
onUpdate: "CASCADE"
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
name: `fk_${pivotName}_${targetColName}`,
|
|
721
|
+
columns: [targetColName],
|
|
722
|
+
referencesTable: targetTable,
|
|
723
|
+
referencesColumns: ["id"],
|
|
724
|
+
onDelete: assocProp.onDelete ?? "CASCADE",
|
|
725
|
+
onUpdate: "CASCADE"
|
|
726
|
+
}
|
|
727
|
+
];
|
|
728
|
+
const indexes = [
|
|
729
|
+
{
|
|
730
|
+
name: `idx_${pivotName}_unique`,
|
|
731
|
+
columns: [sourceColName, targetColName],
|
|
732
|
+
unique: true
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
name: `idx_${pivotName}_${sourceColName}`,
|
|
736
|
+
columns: [sourceColName],
|
|
737
|
+
unique: false
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
name: `idx_${pivotName}_${targetColName}`,
|
|
741
|
+
columns: [targetColName],
|
|
742
|
+
unique: false
|
|
743
|
+
}
|
|
744
|
+
];
|
|
745
|
+
return {
|
|
746
|
+
name: pivotName,
|
|
747
|
+
columns,
|
|
748
|
+
foreignKeys,
|
|
749
|
+
indexes
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function generateMorphPivotTable(sourceSchema, _propertyName, property, allSchemas, options) {
|
|
753
|
+
if (property.type !== "Association") {
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
const assocProp = property;
|
|
757
|
+
if (assocProp.relation !== "MorphToMany" || !assocProp.target) {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
const targetSchema = allSchemas[assocProp.target];
|
|
761
|
+
if (!targetSchema) {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
const dialect = options.dialect;
|
|
765
|
+
const morphName = toSnakeCase(assocProp.target).replace(/s$/, "") + "ables";
|
|
766
|
+
const pivotName = assocProp.joinTable ?? morphName;
|
|
767
|
+
const targetIdType = targetSchema.options?.idType ?? "BigInt";
|
|
768
|
+
const targetColName = `${toSnakeCase(assocProp.target)}_id`;
|
|
769
|
+
const morphTypeName = `${morphName.replace(/s$/, "")}_type`;
|
|
770
|
+
const morphIdName = `${morphName.replace(/s$/, "")}_id`;
|
|
771
|
+
const morphTargets = [sourceSchema.name];
|
|
772
|
+
for (const [schemaName, schema] of Object.entries(allSchemas)) {
|
|
773
|
+
if (schemaName === sourceSchema.name) continue;
|
|
774
|
+
if (!schema.properties) continue;
|
|
775
|
+
for (const prop of Object.values(schema.properties)) {
|
|
776
|
+
const p = prop;
|
|
777
|
+
if (p.type === "Association" && p.relation === "MorphToMany" && p.target === assocProp.target) {
|
|
778
|
+
if (!morphTargets.includes(schemaName)) {
|
|
779
|
+
morphTargets.push(schemaName);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
let useUuid = false;
|
|
785
|
+
for (const targetName of morphTargets) {
|
|
786
|
+
const schema = allSchemas[targetName];
|
|
787
|
+
if (schema?.options?.idType === "Uuid") {
|
|
788
|
+
useUuid = true;
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
const enumInfo = getEnumType(morphTargets, dialect, `${morphTypeName}_enum`);
|
|
793
|
+
const morphIdType = useUuid ? dialect === "mysql" ? "CHAR(36)" : dialect === "postgresql" ? "UUID" : "TEXT" : dialect === "mysql" ? "BIGINT UNSIGNED" : dialect === "postgresql" ? "BIGINT" : "INTEGER";
|
|
794
|
+
const columns = [
|
|
795
|
+
{
|
|
796
|
+
name: targetColName,
|
|
797
|
+
type: getForeignKeyType(targetIdType, dialect),
|
|
798
|
+
nullable: false,
|
|
799
|
+
primaryKey: false,
|
|
800
|
+
autoIncrement: false,
|
|
801
|
+
unique: false,
|
|
802
|
+
unsigned: dialect === "mysql" && ["Int", "BigInt"].includes(targetIdType)
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
name: morphTypeName,
|
|
806
|
+
type: enumInfo.type,
|
|
807
|
+
nullable: false,
|
|
808
|
+
primaryKey: false,
|
|
809
|
+
autoIncrement: false,
|
|
810
|
+
unique: false,
|
|
811
|
+
unsigned: false
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
name: morphIdName,
|
|
815
|
+
type: morphIdType,
|
|
816
|
+
nullable: false,
|
|
817
|
+
primaryKey: false,
|
|
818
|
+
autoIncrement: false,
|
|
819
|
+
unique: false,
|
|
820
|
+
unsigned: dialect === "mysql" && !useUuid
|
|
821
|
+
}
|
|
822
|
+
];
|
|
823
|
+
const foreignKeys = [
|
|
824
|
+
{
|
|
825
|
+
name: `fk_${pivotName}_${targetColName}`,
|
|
826
|
+
columns: [targetColName],
|
|
827
|
+
referencesTable: toTableName(assocProp.target),
|
|
828
|
+
referencesColumns: ["id"],
|
|
829
|
+
onDelete: assocProp.onDelete ?? "CASCADE",
|
|
830
|
+
onUpdate: "CASCADE"
|
|
831
|
+
}
|
|
832
|
+
];
|
|
833
|
+
const indexes = [
|
|
834
|
+
{
|
|
835
|
+
name: `idx_${pivotName}_unique`,
|
|
836
|
+
columns: [targetColName, morphTypeName, morphIdName],
|
|
837
|
+
unique: true
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
name: `idx_${pivotName}_${targetColName}`,
|
|
841
|
+
columns: [targetColName],
|
|
842
|
+
unique: false
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
name: `idx_${pivotName}_morphable`,
|
|
846
|
+
columns: [morphTypeName, morphIdName],
|
|
847
|
+
unique: false
|
|
848
|
+
}
|
|
849
|
+
];
|
|
850
|
+
return {
|
|
851
|
+
name: pivotName,
|
|
852
|
+
columns,
|
|
853
|
+
foreignKeys,
|
|
854
|
+
indexes
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// src/migration/generator.ts
|
|
859
|
+
var DIALECT_INCOMPATIBLE_TYPES = {
|
|
860
|
+
Point: {
|
|
861
|
+
dialects: ["sqlite"],
|
|
862
|
+
reason: "SQLite does not support native spatial types. Point will be stored as TEXT (JSON), which is incompatible with MySQL/PostgreSQL spatial functions (ST_Distance, ST_Within, etc.). Use Coordinates type for cross-database compatibility."
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
var DIALECT_INCOMPATIBLE_INDEX_TYPES = {
|
|
866
|
+
gin: {
|
|
867
|
+
dialects: ["mysql", "sqlite"],
|
|
868
|
+
reason: "GIN (Generalized Inverted Index) is PostgreSQL-specific.",
|
|
869
|
+
suggestion: 'For fulltext search, use type: "fulltext" instead. For JSONB indexing, this only works in PostgreSQL.'
|
|
870
|
+
},
|
|
871
|
+
gist: {
|
|
872
|
+
dialects: ["mysql", "sqlite"],
|
|
873
|
+
reason: "GiST (Generalized Search Tree) is PostgreSQL-specific.",
|
|
874
|
+
suggestion: 'For spatial data, use type: "spatial" which works on both MySQL and PostgreSQL.'
|
|
875
|
+
},
|
|
876
|
+
fulltext: {
|
|
877
|
+
dialects: ["sqlite"],
|
|
878
|
+
reason: "SQLite does not support native fulltext indexes. FTS requires virtual tables which are not auto-generated.",
|
|
879
|
+
suggestion: "Consider using a regular index or implementing SQLite FTS manually."
|
|
880
|
+
},
|
|
881
|
+
spatial: {
|
|
882
|
+
dialects: ["sqlite"],
|
|
883
|
+
reason: "SQLite does not support spatial indexes.",
|
|
884
|
+
suggestion: "Use Coordinates type with regular indexes for cross-database compatibility."
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
function validateSchemaCompatibility(schemas, dialect) {
|
|
888
|
+
const errors = [];
|
|
889
|
+
for (const [schemaName, schema] of Object.entries(schemas)) {
|
|
890
|
+
if (schema.kind === "enum") continue;
|
|
891
|
+
if (schema.properties) {
|
|
892
|
+
for (const [propName, property] of Object.entries(schema.properties)) {
|
|
893
|
+
const propType = property.type;
|
|
894
|
+
const incompatibility = DIALECT_INCOMPATIBLE_TYPES[propType];
|
|
895
|
+
if (incompatibility && incompatibility.dialects.includes(dialect)) {
|
|
896
|
+
errors.push(
|
|
897
|
+
`Schema "${schemaName}", property "${propName}": Type "${propType}" is not supported in ${dialect}. ${incompatibility.reason}`
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
if (schema.options?.indexes) {
|
|
903
|
+
for (const indexDef of schema.options.indexes) {
|
|
904
|
+
if (indexDef.type) {
|
|
905
|
+
const incompatibility = DIALECT_INCOMPATIBLE_INDEX_TYPES[indexDef.type];
|
|
906
|
+
if (incompatibility && incompatibility.dialects.includes(dialect)) {
|
|
907
|
+
const indexName = indexDef.name ?? `index on [${indexDef.columns.join(", ")}]`;
|
|
908
|
+
let message = `Schema "${schemaName}", index "${indexName}": Index type "${indexDef.type}" is not supported in ${dialect}. ${incompatibility.reason}`;
|
|
909
|
+
if (incompatibility.suggestion) {
|
|
910
|
+
message += ` ${incompatibility.suggestion}`;
|
|
911
|
+
}
|
|
912
|
+
errors.push(message);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (errors.length > 0) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
`SQL Generator: Incompatible types detected for dialect "${dialect}":
|
|
921
|
+
|
|
922
|
+
` + errors.map((e, i) => `${i + 1}. ${e}`).join("\n\n") + "\n\nTo fix: Either change the type/index or use a compatible dialect."
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function resolveOptions(options) {
|
|
927
|
+
return {
|
|
928
|
+
dialect: options?.dialect ?? "mysql",
|
|
929
|
+
ifNotExists: options?.ifNotExists ?? true,
|
|
930
|
+
generateDown: options?.generateDown ?? true,
|
|
931
|
+
startVersion: options?.startVersion ?? 1,
|
|
932
|
+
versionPadding: options?.versionPadding ?? 4
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
function formatVersion(version, padding) {
|
|
936
|
+
return String(version).padStart(padding, "0");
|
|
937
|
+
}
|
|
938
|
+
function generateFileName(version, name, padding) {
|
|
939
|
+
const versionStr = formatVersion(version, padding);
|
|
940
|
+
return `${versionStr}_${name}.sql`;
|
|
941
|
+
}
|
|
942
|
+
function generateCreateTableSql(table, options) {
|
|
943
|
+
const lines = [];
|
|
944
|
+
const dialect = options.dialect;
|
|
945
|
+
lines.push(`-- Migration: Create ${table.name} table`);
|
|
946
|
+
lines.push(`-- Generated by @famgia/omnify-sql`);
|
|
947
|
+
lines.push("");
|
|
948
|
+
lines.push(formatCreateTable(table, dialect, { ifNotExists: options.ifNotExists }));
|
|
949
|
+
lines.push("");
|
|
950
|
+
const indexStatements = formatIndexes(table, dialect);
|
|
951
|
+
if (indexStatements.length > 0) {
|
|
952
|
+
lines.push("-- Indexes");
|
|
953
|
+
lines.push(...indexStatements);
|
|
954
|
+
lines.push("");
|
|
955
|
+
}
|
|
956
|
+
const commentStatements = formatColumnComments(table, dialect);
|
|
957
|
+
if (commentStatements.length > 0) {
|
|
958
|
+
lines.push("-- Column comments");
|
|
959
|
+
lines.push(...commentStatements);
|
|
960
|
+
lines.push("");
|
|
961
|
+
}
|
|
962
|
+
if (options.generateDown) {
|
|
963
|
+
lines.push("-- Down migration");
|
|
964
|
+
lines.push(`-- ${formatDropTable(table.name, dialect, { ifExists: true, cascade: true })}`);
|
|
965
|
+
}
|
|
966
|
+
return lines.join("\n");
|
|
967
|
+
}
|
|
968
|
+
function topologicalSort(schemas) {
|
|
969
|
+
const sorted = [];
|
|
970
|
+
const visited = /* @__PURE__ */ new Set();
|
|
971
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
972
|
+
function visit(schemaName) {
|
|
973
|
+
if (visited.has(schemaName)) return;
|
|
974
|
+
if (visiting.has(schemaName)) {
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
visiting.add(schemaName);
|
|
978
|
+
const schema = schemas[schemaName];
|
|
979
|
+
if (!schema) return;
|
|
980
|
+
if (schema.properties) {
|
|
981
|
+
for (const property of Object.values(schema.properties)) {
|
|
982
|
+
if (property.type === "Association") {
|
|
983
|
+
const assocProp = property;
|
|
984
|
+
if (["ManyToOne", "OneToOne"].includes(assocProp.relation ?? "") && assocProp.target) {
|
|
985
|
+
visit(assocProp.target);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
visiting.delete(schemaName);
|
|
991
|
+
visited.add(schemaName);
|
|
992
|
+
sorted.push(schema);
|
|
993
|
+
}
|
|
994
|
+
for (const schemaName of Object.keys(schemas)) {
|
|
995
|
+
visit(schemaName);
|
|
996
|
+
}
|
|
997
|
+
return sorted;
|
|
998
|
+
}
|
|
999
|
+
function generateMigrations(schemas, options) {
|
|
1000
|
+
const resolved = resolveOptions(options);
|
|
1001
|
+
validateSchemaCompatibility(schemas, resolved.dialect);
|
|
1002
|
+
const migrations = [];
|
|
1003
|
+
let version = resolved.startVersion;
|
|
1004
|
+
const objectSchemas = {};
|
|
1005
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
1006
|
+
if (schema.kind !== "enum") {
|
|
1007
|
+
objectSchemas[name] = schema;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
const sortedSchemas = topologicalSort(objectSchemas);
|
|
1011
|
+
const createdPivots = /* @__PURE__ */ new Set();
|
|
1012
|
+
for (const schema of sortedSchemas) {
|
|
1013
|
+
const table = schemaToTable(schema, schemas, resolved);
|
|
1014
|
+
migrations.push({
|
|
1015
|
+
version,
|
|
1016
|
+
name: `create_${table.name}`,
|
|
1017
|
+
fileName: generateFileName(version, `create_${table.name}`, resolved.versionPadding),
|
|
1018
|
+
content: generateCreateTableSql(table, resolved),
|
|
1019
|
+
tables: [table.name],
|
|
1020
|
+
type: "create"
|
|
1021
|
+
});
|
|
1022
|
+
version++;
|
|
1023
|
+
}
|
|
1024
|
+
for (const schema of sortedSchemas) {
|
|
1025
|
+
if (!schema.properties) continue;
|
|
1026
|
+
for (const [propName, property] of Object.entries(schema.properties)) {
|
|
1027
|
+
const pivotTable = generatePivotTable(schema, propName, property, schemas, resolved);
|
|
1028
|
+
if (pivotTable && !createdPivots.has(pivotTable.name)) {
|
|
1029
|
+
createdPivots.add(pivotTable.name);
|
|
1030
|
+
migrations.push({
|
|
1031
|
+
version,
|
|
1032
|
+
name: `create_${pivotTable.name}`,
|
|
1033
|
+
fileName: generateFileName(version, `create_${pivotTable.name}`, resolved.versionPadding),
|
|
1034
|
+
content: generateCreateTableSql(pivotTable, resolved),
|
|
1035
|
+
tables: [pivotTable.name],
|
|
1036
|
+
type: "pivot"
|
|
1037
|
+
});
|
|
1038
|
+
version++;
|
|
1039
|
+
}
|
|
1040
|
+
const morphPivot = generateMorphPivotTable(schema, propName, property, schemas, resolved);
|
|
1041
|
+
if (morphPivot && !createdPivots.has(morphPivot.name)) {
|
|
1042
|
+
createdPivots.add(morphPivot.name);
|
|
1043
|
+
migrations.push({
|
|
1044
|
+
version,
|
|
1045
|
+
name: `create_${morphPivot.name}`,
|
|
1046
|
+
fileName: generateFileName(version, `create_${morphPivot.name}`, resolved.versionPadding),
|
|
1047
|
+
content: generateCreateTableSql(morphPivot, resolved),
|
|
1048
|
+
tables: [morphPivot.name],
|
|
1049
|
+
type: "pivot"
|
|
1050
|
+
});
|
|
1051
|
+
version++;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return migrations;
|
|
1056
|
+
}
|
|
1057
|
+
function generateMigrationFromSchema(schema, allSchemas, options) {
|
|
1058
|
+
const resolved = resolveOptions(options);
|
|
1059
|
+
const version = options?.version ?? resolved.startVersion;
|
|
1060
|
+
const table = schemaToTable(schema, allSchemas, resolved);
|
|
1061
|
+
return {
|
|
1062
|
+
version,
|
|
1063
|
+
name: `create_${table.name}`,
|
|
1064
|
+
fileName: generateFileName(version, `create_${table.name}`, resolved.versionPadding),
|
|
1065
|
+
content: generateCreateTableSql(table, resolved),
|
|
1066
|
+
tables: [table.name],
|
|
1067
|
+
type: "create"
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
function generateDropMigration(tableName, options) {
|
|
1071
|
+
const resolved = resolveOptions(options);
|
|
1072
|
+
const version = options?.version ?? resolved.startVersion;
|
|
1073
|
+
const isTableName = tableName.includes("_") || tableName.endsWith("s") || tableName.endsWith("es");
|
|
1074
|
+
const snakeTable = isTableName ? tableName : toTableName(tableName);
|
|
1075
|
+
const dropSql = formatDropTable(snakeTable, resolved.dialect, { ifExists: true, cascade: true });
|
|
1076
|
+
const lines = [
|
|
1077
|
+
`-- Migration: Drop ${snakeTable} table`,
|
|
1078
|
+
"-- Generated by @famgia/omnify-sql",
|
|
1079
|
+
"",
|
|
1080
|
+
dropSql
|
|
1081
|
+
];
|
|
1082
|
+
return {
|
|
1083
|
+
version,
|
|
1084
|
+
name: `drop_${snakeTable}`,
|
|
1085
|
+
fileName: generateFileName(version, `drop_${snakeTable}`, resolved.versionPadding),
|
|
1086
|
+
content: lines.join("\n"),
|
|
1087
|
+
tables: [snakeTable],
|
|
1088
|
+
type: "drop"
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
function getMigrationPath(migration, basePath = "migrations") {
|
|
1092
|
+
return `${basePath}/${migration.fileName}`;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
export {
|
|
1096
|
+
quoteIdentifier,
|
|
1097
|
+
quoteString,
|
|
1098
|
+
formatColumn,
|
|
1099
|
+
formatForeignKey,
|
|
1100
|
+
formatIndex,
|
|
1101
|
+
formatCreateTable,
|
|
1102
|
+
formatDropTable,
|
|
1103
|
+
getSqlType,
|
|
1104
|
+
getPrimaryKeyType,
|
|
1105
|
+
getForeignKeyType,
|
|
1106
|
+
getEnumType,
|
|
1107
|
+
toSnakeCase,
|
|
1108
|
+
toTableName,
|
|
1109
|
+
toColumnName,
|
|
1110
|
+
schemaToTable,
|
|
1111
|
+
generatePivotTable,
|
|
1112
|
+
generateMorphPivotTable,
|
|
1113
|
+
generateMigrations,
|
|
1114
|
+
generateMigrationFromSchema,
|
|
1115
|
+
generateDropMigration,
|
|
1116
|
+
getMigrationPath
|
|
1117
|
+
};
|
|
1118
|
+
//# sourceMappingURL=chunk-V7HEGSN4.js.map
|