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.
Files changed (46) hide show
  1. package/README.md +194 -26
  2. package/dist/cjs/cli/config.js +5 -15
  3. package/dist/cjs/cli/index.js +240 -41
  4. package/dist/cjs/cli/migrate.js +71 -46
  5. package/dist/cjs/cli/ui.js +5 -9
  6. package/dist/cjs/client.js +109 -46
  7. package/dist/cjs/errors.js +293 -0
  8. package/dist/cjs/generate.js +33 -13
  9. package/dist/cjs/index.js +39 -20
  10. package/dist/cjs/introspect.js +3 -5
  11. package/dist/cjs/pipeline.js +9 -2
  12. package/dist/cjs/query.js +442 -109
  13. package/dist/cjs/schema-builder.js +93 -24
  14. package/dist/cjs/schema-sql.js +157 -19
  15. package/dist/cjs/schema.js +5 -2
  16. package/dist/cjs/serverless.js +87 -176
  17. package/dist/cli/config.js +6 -16
  18. package/dist/cli/index.js +245 -46
  19. package/dist/cli/migrate.d.ts +6 -1
  20. package/dist/cli/migrate.js +72 -47
  21. package/dist/cli/ui.js +5 -9
  22. package/dist/client.d.ts +77 -4
  23. package/dist/client.js +109 -46
  24. package/dist/errors.d.ts +138 -0
  25. package/dist/errors.js +278 -0
  26. package/dist/generate.d.ts +1 -1
  27. package/dist/generate.js +36 -16
  28. package/dist/index.d.ts +11 -9
  29. package/dist/index.js +16 -12
  30. package/dist/introspect.d.ts +1 -1
  31. package/dist/introspect.js +4 -6
  32. package/dist/pipeline.d.ts +1 -1
  33. package/dist/pipeline.js +9 -2
  34. package/dist/query.d.ts +257 -36
  35. package/dist/query.js +443 -110
  36. package/dist/schema-builder.d.ts +2 -2
  37. package/dist/schema-builder.js +93 -25
  38. package/dist/schema-sql.d.ts +7 -3
  39. package/dist/schema-sql.js +157 -19
  40. package/dist/schema.d.ts +1 -1
  41. package/dist/schema.js +5 -2
  42. package/dist/serverless.d.ts +91 -139
  43. package/dist/serverless.js +86 -173
  44. package/package.json +33 -16
  45. package/dist/types.d.ts +0 -93
  46. package/dist/types.js +0 -126
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * @batadata/turbine — Schema Builder
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 '@batadata/turbine';
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() { this._config.type = 'BIGSERIAL'; return this; }
121
- bigint() { this._config.type = 'BIGINT'; return this; }
122
- integer() { this._config.type = 'INTEGER'; return this; }
123
- smallint() { this._config.type = 'SMALLINT'; return this; }
124
- text() { this._config.type = 'TEXT'; return this; }
125
- varchar(length) { this._config.type = 'VARCHAR'; this._config.maxLength = length; return this; }
126
- boolean() { this._config.type = 'BOOLEAN'; return this; }
127
- timestamp() { this._config.type = 'TIMESTAMPTZ'; return this; }
128
- date() { this._config.type = 'DATE'; return this; }
129
- json() { this._config.type = 'JSONB'; return this; }
130
- uuid() { this._config.type = 'UUID'; return this; }
131
- real() { this._config.type = 'REAL'; return this; }
132
- doublePrecision() { this._config.type = 'DOUBLE PRECISION'; return this; }
133
- numeric() { this._config.type = 'NUMERIC'; return this; }
134
- bytea() { this._config.type = 'BYTEA'; return this; }
135
- primaryKey() { this._config.isPrimaryKey = true; return this; }
136
- notNull() { this._config.isNotNull = true; return this; }
137
- nullable() { this._config.isNullable = true; return this; }
138
- unique() { this._config.isUnique = true; return this; }
139
- default(val) { this._config.defaultValue = val; return this; }
140
- references(target) { this._config.referencesTarget = target; return this; }
141
- build() { return { ...this._config }; }
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 */
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * @batadata/turbine — Schema SQL Generator
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 nested quotes)
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
- // Also add FK indexes
229
- result.statements.push(...generateForeignKeyIndexes(tableDef));
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
- alterDef.columns.push({ column: snakeName, action: 'add', sql });
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
- ? `VARCHAR(${config.maxLength})`
262
- : config.type;
263
- const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} TYPE ${sqlType};`;
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
- alterDef.columns.push({ column: snakeName, action: 'set_not_null', sql });
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
- alterDef.columns.push({ column: snakeName, action: 'drop_not_null', sql });
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
- alterDef.columns.push({ column: dbColName, action: 'drop', sql });
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') + '\n';
508
+ return `${statements.join('\n\n')}\n`;
371
509
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * @batadata/turbine — Schema metadata types
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) + 'y';
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'))