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