turbine-orm 0.5.0 → 0.7.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.
- package/README.md +194 -26
- package/dist/cjs/cli/config.js +5 -15
- package/dist/cjs/cli/index.js +240 -41
- package/dist/cjs/cli/migrate.js +71 -46
- package/dist/cjs/cli/ui.js +5 -9
- package/dist/cjs/client.js +109 -46
- package/dist/cjs/errors.js +293 -0
- package/dist/cjs/generate.js +33 -13
- package/dist/cjs/index.js +39 -20
- package/dist/cjs/introspect.js +3 -5
- package/dist/cjs/pipeline.js +9 -2
- package/dist/cjs/query.js +442 -109
- package/dist/cjs/schema-builder.js +93 -24
- package/dist/cjs/schema-sql.js +157 -19
- package/dist/cjs/schema.js +5 -2
- package/dist/cjs/serverless.js +87 -176
- package/dist/cli/config.js +6 -16
- package/dist/cli/index.js +245 -46
- package/dist/cli/migrate.d.ts +6 -1
- package/dist/cli/migrate.js +72 -47
- package/dist/cli/ui.js +5 -9
- package/dist/client.d.ts +77 -4
- package/dist/client.js +109 -46
- package/dist/errors.d.ts +138 -0
- package/dist/errors.js +278 -0
- package/dist/generate.d.ts +1 -1
- package/dist/generate.js +36 -16
- package/dist/index.d.ts +11 -9
- package/dist/index.js +16 -12
- package/dist/introspect.d.ts +1 -1
- package/dist/introspect.js +4 -6
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +9 -2
- package/dist/query.d.ts +257 -36
- package/dist/query.js +443 -110
- package/dist/schema-builder.d.ts +2 -2
- package/dist/schema-builder.js +93 -25
- package/dist/schema-sql.d.ts +7 -3
- package/dist/schema-sql.js +157 -19
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +5 -2
- package/dist/serverless.d.ts +91 -139
- package/dist/serverless.js +86 -173
- package/package.json +33 -16
- package/dist/types.d.ts +0 -93
- package/dist/types.js +0 -126
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* turbine-orm — Schema Builder
|
|
4
4
|
*
|
|
5
5
|
* TypeScript-first schema definition API. Define your database schema
|
|
6
6
|
* as plain objects — no method chaining, no DSL. Fully type-checked,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
10
|
* ```ts
|
|
11
|
-
* import { defineSchema } from '
|
|
11
|
+
* import { defineSchema } from 'turbine-orm';
|
|
12
12
|
*
|
|
13
13
|
* export default defineSchema({
|
|
14
14
|
* users: {
|
|
@@ -47,6 +47,9 @@ const TYPE_MAP = {
|
|
|
47
47
|
};
|
|
48
48
|
/** Convert a user-facing ColumnDef to the internal ColumnConfig */
|
|
49
49
|
function resolveColumn(def) {
|
|
50
|
+
if (!(def.type in TYPE_MAP)) {
|
|
51
|
+
throw new Error(`Invalid column type "${def.type}". Valid types: ${Object.keys(TYPE_MAP).join(', ')}`);
|
|
52
|
+
}
|
|
50
53
|
return {
|
|
51
54
|
type: TYPE_MAP[def.type],
|
|
52
55
|
isPrimaryKey: def.primaryKey ?? false,
|
|
@@ -117,28 +120,94 @@ class ColumnBuilder {
|
|
|
117
120
|
maxLength: null,
|
|
118
121
|
};
|
|
119
122
|
}
|
|
120
|
-
serial() {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
123
|
+
serial() {
|
|
124
|
+
this._config.type = 'BIGSERIAL';
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
bigint() {
|
|
128
|
+
this._config.type = 'BIGINT';
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
integer() {
|
|
132
|
+
this._config.type = 'INTEGER';
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
smallint() {
|
|
136
|
+
this._config.type = 'SMALLINT';
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
text() {
|
|
140
|
+
this._config.type = 'TEXT';
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
varchar(length) {
|
|
144
|
+
this._config.type = 'VARCHAR';
|
|
145
|
+
this._config.maxLength = length;
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
148
|
+
boolean() {
|
|
149
|
+
this._config.type = 'BOOLEAN';
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
timestamp() {
|
|
153
|
+
this._config.type = 'TIMESTAMPTZ';
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
date() {
|
|
157
|
+
this._config.type = 'DATE';
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
json() {
|
|
161
|
+
this._config.type = 'JSONB';
|
|
162
|
+
return this;
|
|
163
|
+
}
|
|
164
|
+
uuid() {
|
|
165
|
+
this._config.type = 'UUID';
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
real() {
|
|
169
|
+
this._config.type = 'REAL';
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
172
|
+
doublePrecision() {
|
|
173
|
+
this._config.type = 'DOUBLE PRECISION';
|
|
174
|
+
return this;
|
|
175
|
+
}
|
|
176
|
+
numeric() {
|
|
177
|
+
this._config.type = 'NUMERIC';
|
|
178
|
+
return this;
|
|
179
|
+
}
|
|
180
|
+
bytea() {
|
|
181
|
+
this._config.type = 'BYTEA';
|
|
182
|
+
return this;
|
|
183
|
+
}
|
|
184
|
+
primaryKey() {
|
|
185
|
+
this._config.isPrimaryKey = true;
|
|
186
|
+
return this;
|
|
187
|
+
}
|
|
188
|
+
notNull() {
|
|
189
|
+
this._config.isNotNull = true;
|
|
190
|
+
return this;
|
|
191
|
+
}
|
|
192
|
+
nullable() {
|
|
193
|
+
this._config.isNullable = true;
|
|
194
|
+
return this;
|
|
195
|
+
}
|
|
196
|
+
unique() {
|
|
197
|
+
this._config.isUnique = true;
|
|
198
|
+
return this;
|
|
199
|
+
}
|
|
200
|
+
default(val) {
|
|
201
|
+
this._config.defaultValue = val;
|
|
202
|
+
return this;
|
|
203
|
+
}
|
|
204
|
+
references(target) {
|
|
205
|
+
this._config.referencesTarget = target;
|
|
206
|
+
return this;
|
|
207
|
+
}
|
|
208
|
+
build() {
|
|
209
|
+
return { ...this._config };
|
|
210
|
+
}
|
|
142
211
|
}
|
|
143
212
|
exports.ColumnBuilder = ColumnBuilder;
|
|
144
213
|
/** @deprecated Use defineSchema() with plain objects instead */
|
package/dist/cjs/schema-sql.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* turbine-orm — Schema SQL Generator
|
|
4
4
|
*
|
|
5
5
|
* Converts a SchemaDef (from defineSchema) into executable DDL statements.
|
|
6
6
|
* Also provides diff and push commands for syncing schema to a live database.
|
|
@@ -14,8 +14,8 @@ exports.schemaDiff = schemaDiff;
|
|
|
14
14
|
exports.schemaPush = schemaPush;
|
|
15
15
|
exports.schemaToSQLString = schemaToSQLString;
|
|
16
16
|
const pg_1 = __importDefault(require("pg"));
|
|
17
|
-
const schema_js_1 = require("./schema.js");
|
|
18
17
|
const query_js_1 = require("./query.js");
|
|
18
|
+
const schema_js_1 = require("./schema.js");
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
20
|
// SQL Generation — SchemaDef → CREATE TABLE statements
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
@@ -154,9 +154,7 @@ function normalizeDefault(val) {
|
|
|
154
154
|
return upper;
|
|
155
155
|
}
|
|
156
156
|
// Known SQL function calls: NOW(), CURRENT_TIMESTAMP, CURRENT_DATE, CURRENT_TIME, GEN_RANDOM_UUID()
|
|
157
|
-
const allowedFunctions = [
|
|
158
|
-
'NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME', 'GEN_RANDOM_UUID()',
|
|
159
|
-
];
|
|
157
|
+
const allowedFunctions = ['NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME', 'GEN_RANDOM_UUID()'];
|
|
160
158
|
if (allowedFunctions.includes(upper)) {
|
|
161
159
|
return upper;
|
|
162
160
|
}
|
|
@@ -164,8 +162,12 @@ function normalizeDefault(val) {
|
|
|
164
162
|
if (/^-?\d+(\.\d+)?$/.test(val.trim())) {
|
|
165
163
|
return val.trim();
|
|
166
164
|
}
|
|
167
|
-
// Simple single-quoted string literals (no
|
|
165
|
+
// Simple single-quoted string literals (no semicolons, no SQL statement keywords)
|
|
168
166
|
if (/^'[^']*'$/.test(val.trim())) {
|
|
167
|
+
const inner = val.trim().slice(1, -1);
|
|
168
|
+
if (/[;]/.test(inner) || /\b(DROP|ALTER|CREATE|INSERT|UPDATE|DELETE|GRANT|REVOKE|TRUNCATE)\b/i.test(inner)) {
|
|
169
|
+
throw new Error(`Suspicious default value: ${val}. String literals must not contain SQL statements.`);
|
|
170
|
+
}
|
|
169
171
|
return val.trim();
|
|
170
172
|
}
|
|
171
173
|
throw new Error(`Unsupported default value: ${val}. Use a SQL function, numeric, string literal, or NULL.`);
|
|
@@ -216,8 +218,30 @@ async function schemaDiff(schema, connectionString) {
|
|
|
216
218
|
maxLength: row.character_maximum_length,
|
|
217
219
|
};
|
|
218
220
|
}
|
|
221
|
+
// Get single-column UNIQUE constraints (excluding PKs)
|
|
222
|
+
const uniqueResult = await client.query(`SELECT tc.table_name, tc.constraint_name, kcu.column_name
|
|
223
|
+
FROM information_schema.table_constraints tc
|
|
224
|
+
JOIN information_schema.key_column_usage kcu
|
|
225
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
226
|
+
AND tc.table_schema = kcu.table_schema
|
|
227
|
+
WHERE tc.table_schema = 'public'
|
|
228
|
+
AND tc.constraint_type = 'UNIQUE'
|
|
229
|
+
AND tc.constraint_name IN (
|
|
230
|
+
SELECT constraint_name
|
|
231
|
+
FROM information_schema.key_column_usage
|
|
232
|
+
WHERE table_schema = 'public'
|
|
233
|
+
GROUP BY constraint_name
|
|
234
|
+
HAVING COUNT(*) = 1
|
|
235
|
+
)`);
|
|
236
|
+
// Map: table → column → constraint_name for single-col uniques
|
|
237
|
+
const dbUniques = {};
|
|
238
|
+
for (const row of uniqueResult.rows) {
|
|
239
|
+
if (!dbUniques[row.table_name])
|
|
240
|
+
dbUniques[row.table_name] = {};
|
|
241
|
+
dbUniques[row.table_name][row.column_name] = row.constraint_name;
|
|
242
|
+
}
|
|
219
243
|
const schemaTableNames = new Set(Object.keys(schema.tables));
|
|
220
|
-
const result = { create: [], alter: [], drop: [], statements: [] };
|
|
244
|
+
const result = { create: [], alter: [], drop: [], statements: [], reverseStatements: [] };
|
|
221
245
|
// Tables to create (in schema but not in DB)
|
|
222
246
|
const sorted = topologicalSort(schema);
|
|
223
247
|
for (const tableName of sorted) {
|
|
@@ -225,8 +249,10 @@ async function schemaDiff(schema, connectionString) {
|
|
|
225
249
|
const tableDef = schema.tables[tableName];
|
|
226
250
|
result.create.push(tableDef);
|
|
227
251
|
result.statements.push(generateCreateTable(tableDef));
|
|
228
|
-
|
|
229
|
-
result.statements.push(...
|
|
252
|
+
const fkIndexes = generateForeignKeyIndexes(tableDef);
|
|
253
|
+
result.statements.push(...fkIndexes);
|
|
254
|
+
// Reverse: DROP TABLE (with indexes — they drop automatically)
|
|
255
|
+
result.reverseStatements.unshift(`DROP TABLE IF EXISTS ${(0, query_js_1.quoteIdent)(tableName)} CASCADE;`);
|
|
230
256
|
}
|
|
231
257
|
}
|
|
232
258
|
// Tables to drop (in DB but not in schema)
|
|
@@ -242,6 +268,7 @@ async function schemaDiff(schema, connectionString) {
|
|
|
242
268
|
continue;
|
|
243
269
|
const tableDef = schema.tables[tableName];
|
|
244
270
|
const dbCols = dbColumns[tableName] ?? {};
|
|
271
|
+
const tableUniques = dbUniques[tableName] ?? {};
|
|
245
272
|
const alterDef = { table: tableName, columns: [] };
|
|
246
273
|
for (const [fieldName, config] of Object.entries(tableDef.columns)) {
|
|
247
274
|
const snakeName = (0, schema_js_1.camelToSnake)(fieldName);
|
|
@@ -250,32 +277,93 @@ async function schemaDiff(schema, connectionString) {
|
|
|
250
277
|
// Column exists in schema but not in DB — ADD COLUMN
|
|
251
278
|
const colDef = generateColumnDef(fieldName, config);
|
|
252
279
|
const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ADD COLUMN ${colDef};`;
|
|
253
|
-
|
|
280
|
+
const reverseSql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} DROP COLUMN ${(0, query_js_1.quoteIdent)(snakeName)};`;
|
|
281
|
+
alterDef.columns.push({ column: snakeName, action: 'add', sql, reverseSql });
|
|
254
282
|
result.statements.push(sql);
|
|
283
|
+
result.reverseStatements.unshift(reverseSql);
|
|
255
284
|
continue;
|
|
256
285
|
}
|
|
257
286
|
// Check type mismatch
|
|
258
287
|
const expectedUdt = schemaTypeToUdt(config);
|
|
259
288
|
if (expectedUdt && dbCol.udtName !== expectedUdt) {
|
|
260
|
-
const sqlType = config.type === 'VARCHAR' && config.maxLength
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
alterDef.columns.push({ column: snakeName, action: 'alter_type', sql });
|
|
289
|
+
const sqlType = config.type === 'VARCHAR' && config.maxLength ? `VARCHAR(${config.maxLength})` : config.type;
|
|
290
|
+
const oldSqlType = udtToSqlType(dbCol.udtName, dbCol.maxLength);
|
|
291
|
+
const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} TYPE ${sqlType} USING ${(0, query_js_1.quoteIdent)(snakeName)}::${sqlType};`;
|
|
292
|
+
const reverseSql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} TYPE ${oldSqlType} USING ${(0, query_js_1.quoteIdent)(snakeName)}::${oldSqlType};`;
|
|
293
|
+
alterDef.columns.push({ column: snakeName, action: 'alter_type', sql, reverseSql });
|
|
265
294
|
result.statements.push(sql);
|
|
295
|
+
result.reverseStatements.unshift(reverseSql);
|
|
266
296
|
}
|
|
267
297
|
// Check NOT NULL mismatch
|
|
268
298
|
const shouldBeNotNull = config.isNotNull || config.isPrimaryKey || config.type === 'BIGSERIAL';
|
|
269
299
|
const isCurrentlyNullable = dbCol.isNullable;
|
|
270
300
|
if (shouldBeNotNull && isCurrentlyNullable && !config.isNullable) {
|
|
271
301
|
const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} SET NOT NULL;`;
|
|
272
|
-
|
|
302
|
+
const reverseSql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} DROP NOT NULL;`;
|
|
303
|
+
alterDef.columns.push({ column: snakeName, action: 'set_not_null', sql, reverseSql });
|
|
273
304
|
result.statements.push(sql);
|
|
305
|
+
result.reverseStatements.unshift(reverseSql);
|
|
274
306
|
}
|
|
275
307
|
else if (!shouldBeNotNull && !isCurrentlyNullable && config.isNullable) {
|
|
276
308
|
const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} DROP NOT NULL;`;
|
|
277
|
-
|
|
309
|
+
const reverseSql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} SET NOT NULL;`;
|
|
310
|
+
alterDef.columns.push({ column: snakeName, action: 'drop_not_null', sql, reverseSql });
|
|
278
311
|
result.statements.push(sql);
|
|
312
|
+
result.reverseStatements.unshift(reverseSql);
|
|
313
|
+
}
|
|
314
|
+
// Check DEFAULT value mismatch
|
|
315
|
+
const isSerial = config.type === 'BIGSERIAL';
|
|
316
|
+
if (!isSerial) {
|
|
317
|
+
const schemaDefault = config.defaultValue ? normalizeDefault(config.defaultValue) : null;
|
|
318
|
+
const dbDefault = dbCol.columnDefault;
|
|
319
|
+
if (schemaDefault && !dbDefault) {
|
|
320
|
+
// Schema has default, DB doesn't
|
|
321
|
+
const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} SET DEFAULT ${schemaDefault};`;
|
|
322
|
+
const reverseSql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} DROP DEFAULT;`;
|
|
323
|
+
alterDef.columns.push({ column: snakeName, action: 'set_default', sql, reverseSql });
|
|
324
|
+
result.statements.push(sql);
|
|
325
|
+
result.reverseStatements.unshift(reverseSql);
|
|
326
|
+
}
|
|
327
|
+
else if (!schemaDefault && dbDefault && !isSequenceDefault(dbDefault)) {
|
|
328
|
+
// DB has a non-sequence default, schema doesn't
|
|
329
|
+
const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} DROP DEFAULT;`;
|
|
330
|
+
const reverseSql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} SET DEFAULT ${dbDefault};`;
|
|
331
|
+
alterDef.columns.push({ column: snakeName, action: 'drop_default', sql, reverseSql });
|
|
332
|
+
result.statements.push(sql);
|
|
333
|
+
result.reverseStatements.unshift(reverseSql);
|
|
334
|
+
}
|
|
335
|
+
else if (schemaDefault &&
|
|
336
|
+
dbDefault &&
|
|
337
|
+
!isSequenceDefault(dbDefault) &&
|
|
338
|
+
!defaultsMatch(schemaDefault, dbDefault)) {
|
|
339
|
+
// Both have defaults but they differ
|
|
340
|
+
const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} SET DEFAULT ${schemaDefault};`;
|
|
341
|
+
const reverseSql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} SET DEFAULT ${dbDefault};`;
|
|
342
|
+
alterDef.columns.push({ column: snakeName, action: 'set_default', sql, reverseSql });
|
|
343
|
+
result.statements.push(sql);
|
|
344
|
+
result.reverseStatements.unshift(reverseSql);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// Check UNIQUE constraint mismatch (skip PKs — they're implicitly unique)
|
|
348
|
+
if (!config.isPrimaryKey) {
|
|
349
|
+
const hasDbUnique = snakeName in tableUniques;
|
|
350
|
+
const wantsUnique = config.isUnique === true;
|
|
351
|
+
if (wantsUnique && !hasDbUnique) {
|
|
352
|
+
const constraintName = `${tableName}_${snakeName}_key`;
|
|
353
|
+
const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ADD CONSTRAINT ${(0, query_js_1.quoteIdent)(constraintName)} UNIQUE (${(0, query_js_1.quoteIdent)(snakeName)});`;
|
|
354
|
+
const reverseSql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} DROP CONSTRAINT ${(0, query_js_1.quoteIdent)(constraintName)};`;
|
|
355
|
+
alterDef.columns.push({ column: snakeName, action: 'add_unique', sql, reverseSql });
|
|
356
|
+
result.statements.push(sql);
|
|
357
|
+
result.reverseStatements.unshift(reverseSql);
|
|
358
|
+
}
|
|
359
|
+
else if (!wantsUnique && hasDbUnique) {
|
|
360
|
+
const constraintName = tableUniques[snakeName];
|
|
361
|
+
const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} DROP CONSTRAINT ${(0, query_js_1.quoteIdent)(constraintName)};`;
|
|
362
|
+
const reverseSql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ADD CONSTRAINT ${(0, query_js_1.quoteIdent)(constraintName)} UNIQUE (${(0, query_js_1.quoteIdent)(snakeName)});`;
|
|
363
|
+
alterDef.columns.push({ column: snakeName, action: 'drop_unique', sql, reverseSql });
|
|
364
|
+
result.statements.push(sql);
|
|
365
|
+
result.reverseStatements.unshift(reverseSql);
|
|
366
|
+
}
|
|
279
367
|
}
|
|
280
368
|
}
|
|
281
369
|
// Check for columns in DB that are not in schema
|
|
@@ -283,7 +371,8 @@ async function schemaDiff(schema, connectionString) {
|
|
|
283
371
|
const hasField = Object.entries(tableDef.columns).some(([fieldName]) => (0, schema_js_1.camelToSnake)(fieldName) === dbColName);
|
|
284
372
|
if (!hasField) {
|
|
285
373
|
const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} DROP COLUMN ${(0, query_js_1.quoteIdent)(dbColName)};`;
|
|
286
|
-
|
|
374
|
+
const reverseSql = `-- Cannot auto-reverse DROP COLUMN for "${dbColName}" — add it back manually`;
|
|
375
|
+
alterDef.columns.push({ column: dbColName, action: 'drop', sql, reverseSql });
|
|
287
376
|
// Don't auto-add drops to statements for safety — user must opt in
|
|
288
377
|
}
|
|
289
378
|
}
|
|
@@ -320,6 +409,55 @@ function schemaTypeToUdt(config) {
|
|
|
320
409
|
};
|
|
321
410
|
return map[config.type] ?? null;
|
|
322
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Reverse map: PostgreSQL UDT name → SQL type (for generating reverse ALTER TYPE).
|
|
414
|
+
*/
|
|
415
|
+
function udtToSqlType(udtName, maxLength) {
|
|
416
|
+
const map = {
|
|
417
|
+
int8: 'BIGINT',
|
|
418
|
+
int4: 'INTEGER',
|
|
419
|
+
int2: 'SMALLINT',
|
|
420
|
+
text: 'TEXT',
|
|
421
|
+
varchar: maxLength ? `VARCHAR(${maxLength})` : 'VARCHAR',
|
|
422
|
+
bool: 'BOOLEAN',
|
|
423
|
+
timestamptz: 'TIMESTAMPTZ',
|
|
424
|
+
date: 'DATE',
|
|
425
|
+
jsonb: 'JSONB',
|
|
426
|
+
uuid: 'UUID',
|
|
427
|
+
float4: 'REAL',
|
|
428
|
+
float8: 'DOUBLE PRECISION',
|
|
429
|
+
numeric: 'NUMERIC',
|
|
430
|
+
bytea: 'BYTEA',
|
|
431
|
+
};
|
|
432
|
+
return map[udtName] ?? udtName.toUpperCase();
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Normalize a database default value for comparison.
|
|
436
|
+
* Strips PostgreSQL type casts (e.g. 'free'::text → 'free') and wrapping parens.
|
|
437
|
+
*/
|
|
438
|
+
function normalizeDbDefault(dbDefault) {
|
|
439
|
+
let val = dbDefault;
|
|
440
|
+
// Strip type casts: 'free'::text → 'free', 0::integer → 0
|
|
441
|
+
val = val.replace(/::[\w\s"]+(\[\])?$/g, '').trim();
|
|
442
|
+
// Unwrap parens added by PostgreSQL: ('free') → 'free'
|
|
443
|
+
while (val.startsWith('(') && val.endsWith(')')) {
|
|
444
|
+
val = val.slice(1, -1).trim();
|
|
445
|
+
}
|
|
446
|
+
return val;
|
|
447
|
+
}
|
|
448
|
+
/** Check if a DB default is a sequence default (auto-generated for serial columns). */
|
|
449
|
+
function isSequenceDefault(dbDefault) {
|
|
450
|
+
return dbDefault.includes('nextval(');
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Compare a schema default against a database default, accounting for
|
|
454
|
+
* PostgreSQL's normalization of default values.
|
|
455
|
+
*/
|
|
456
|
+
function defaultsMatch(schemaDefault, dbDefault) {
|
|
457
|
+
const a = schemaDefault.toLowerCase().trim();
|
|
458
|
+
const b = normalizeDbDefault(dbDefault).toLowerCase().trim();
|
|
459
|
+
return a === b;
|
|
460
|
+
}
|
|
323
461
|
/**
|
|
324
462
|
* Push a schema definition to a live database.
|
|
325
463
|
*
|
|
@@ -367,5 +505,5 @@ async function schemaPush(schema, connectionString, options = {}) {
|
|
|
367
505
|
*/
|
|
368
506
|
function schemaToSQLString(schema) {
|
|
369
507
|
const statements = schemaToSQL(schema);
|
|
370
|
-
return statements.join('\n\n')
|
|
508
|
+
return `${statements.join('\n\n')}\n`;
|
|
371
509
|
}
|
package/dist/cjs/schema.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* turbine-orm — Schema metadata types
|
|
4
4
|
*
|
|
5
5
|
* These types represent the introspected database schema at runtime.
|
|
6
6
|
* They're used by the query builder, code generator, and CLI.
|
|
@@ -20,6 +20,9 @@ const PG_TO_TS = {
|
|
|
20
20
|
// Integers
|
|
21
21
|
int2: 'number',
|
|
22
22
|
int4: 'number',
|
|
23
|
+
// int8 maps to `number` for DX (auto-increment IDs, counts, etc.).
|
|
24
|
+
// Values exceeding Number.MAX_SAFE_INTEGER (2^53 - 1) are returned as
|
|
25
|
+
// `string` at runtime to avoid precision loss. See client.ts int8 parser.
|
|
23
26
|
int8: 'number',
|
|
24
27
|
float4: 'number',
|
|
25
28
|
float8: 'number',
|
|
@@ -128,7 +131,7 @@ function snakeToPascal(s) {
|
|
|
128
131
|
/** Naive singularize: "posts" → "post", "categories" → "category" */
|
|
129
132
|
function singularize(s) {
|
|
130
133
|
if (s.endsWith('ies'))
|
|
131
|
-
return s.slice(0, -3)
|
|
134
|
+
return `${s.slice(0, -3)}y`;
|
|
132
135
|
if (s.endsWith('ses') || s.endsWith('xes') || s.endsWith('zes'))
|
|
133
136
|
return s.slice(0, -2);
|
|
134
137
|
if (s.endsWith('s') && !s.endsWith('ss'))
|