Skip to content
This repository was archived by the owner on Mar 1, 2026. It is now read-only.

Commit 2b6354a

Browse files
committed
fix(cli): refactor PostgreSQL type casting and fix index order
Extracts PostgreSQL type casting logic into a reusable helper function to improve maintainability and ensure consistent attribute handling across all field types. Adjusts the table index sorting logic to better preserve the original database creation order while maintaining the priority of unique indexes.
1 parent d318636 commit 2b6354a

3 files changed

Lines changed: 65 additions & 39 deletions

File tree

packages/cli/src/actions/pull/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ export function syncTable({
331331
}
332332

333333
// Sort indexes: unique indexes first, then other indexes
334-
const sortedIndexes = table.indexes.sort((a, b) => {
334+
const sortedIndexes = table.indexes.reverse().sort((a, b) => {
335335
if (a.unique && !b.unique) return -1;
336336
if (!a.unique && b.unique) return 1;
337337
return 0;

packages/cli/src/actions/pull/provider/postgresql.ts

Lines changed: 63 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import type { Attribute, BuiltinType } from '@zenstackhq/language/ast';
2-
import { DataFieldAttributeFactory } from '@zenstackhq/language/factory';
1+
import type { Attribute, BuiltinType, Enum, Expression } from '@zenstackhq/language/ast';
2+
import { AstFactory, DataFieldAttributeFactory, ExpressionBuilder } from '@zenstackhq/language/factory';
33
import { Client } from 'pg';
44
import { getAttributeRef, getDbName, getFunctionRef } from '../utils';
55
import type { IntrospectedEnum, IntrospectedSchema, IntrospectedTable, IntrospectionProvider } from './provider';
6+
import type { ZModelServices } from '@zenstackhq/language';
67

78
export const postgresql: IntrospectionProvider = {
89
isSupportedFeature(feature) {
@@ -112,46 +113,16 @@ export const postgresql: IntrospectionProvider = {
112113
getDefaultValue({ defaultValue, fieldType, services, enums }) {
113114
const val = defaultValue.trim();
114115

115-
// Handle type casts early (PostgreSQL-specific pattern like 'value'::type)
116-
if (val.includes('::')) {
117-
const [value, type] = val
118-
.replace(/'/g, '')
119-
.split('::')
120-
.map((s) => s.trim()) as [string, string];
121-
switch (type) {
122-
case 'character varying':
123-
case 'uuid':
124-
case 'json':
125-
case 'jsonb':
126-
case 'text':
127-
if (value === 'NULL') return null;
128-
return (ab) => ab.StringLiteral.setValue(value);
129-
case 'real':
130-
return (ab) => ab.NumberLiteral.setValue(value);
131-
default: {
132-
const enumDef = enums.find((e) => getDbName(e, true) === type);
133-
if (!enumDef) {
134-
return (ab) =>
135-
ab.InvocationExpr.setFunction(getFunctionRef('dbgenerated', services)).addArg((a) =>
136-
a.setValue((v) => v.StringLiteral.setValue(val)),
137-
);
138-
}
139-
const enumField = enumDef.fields.find((v) => getDbName(v) === value);
140-
if (!enumField) {
141-
throw new Error(
142-
`Enum value ${value} not found in enum ${type} for default value ${defaultValue}`,
143-
);
144-
}
145-
return (ab) => ab.ReferenceExpr.setTarget(enumField);
146-
}
147-
}
148-
}
149-
150116
switch (fieldType) {
151117
case 'DateTime':
152118
if (val === 'CURRENT_TIMESTAMP' || val === 'now()') {
153119
return (ab) => ab.InvocationExpr.setFunction(getFunctionRef('now', services));
154120
}
121+
122+
if (val.includes('::')) {
123+
return typeCastingConvert({defaultValue,enums,val,services});
124+
}
125+
155126
// Fallback to string literal for other DateTime defaults
156127
return (ab) => ab.StringLiteral.setValue(val);
157128

@@ -160,12 +131,21 @@ export const postgresql: IntrospectionProvider = {
160131
if (val.startsWith('nextval(')) {
161132
return (ab) => ab.InvocationExpr.setFunction(getFunctionRef('autoincrement', services));
162133
}
134+
135+
if (val.includes('::')) {
136+
return typeCastingConvert({defaultValue,enums,val,services});
137+
}
138+
163139
if (/^-?\d+$/.test(val)) {
164140
return (ab) => ab.NumberLiteral.setValue(val);
165141
}
166142
break;
167143

168144
case 'Float':
145+
if (val.includes('::')) {
146+
return typeCastingConvert({defaultValue,enums,val,services});
147+
}
148+
169149
if (/^-?\d+\.\d+$/.test(val)) {
170150
const numVal = parseFloat(val);
171151
return (ab) => ab.NumberLiteral.setValue(numVal === Math.floor(numVal) ? numVal.toFixed(1) : String(numVal));
@@ -176,6 +156,10 @@ export const postgresql: IntrospectionProvider = {
176156
break;
177157

178158
case 'Decimal':
159+
if (val.includes('::')) {
160+
return typeCastingConvert({defaultValue,enums,val,services});
161+
}
162+
179163
if (/^-?\d+\.\d+$/.test(val)) {
180164
const numVal = parseFloat(val);
181165
if (numVal === Math.floor(numVal)) {
@@ -198,12 +182,20 @@ export const postgresql: IntrospectionProvider = {
198182
break;
199183

200184
case 'String':
185+
if (val.includes('::')) {
186+
return typeCastingConvert({defaultValue,enums,val,services});
187+
}
188+
201189
if (val.startsWith("'") && val.endsWith("'")) {
202190
return (ab) => ab.StringLiteral.setValue(val.slice(1, -1).replace(/''/g, "'"));
203191
}
204192
break;
205193
}
206194

195+
if (val.includes('::')) {
196+
return typeCastingConvert({defaultValue,enums,val,services});
197+
}
198+
207199
// Fallback handlers for values that don't match field type-specific patterns
208200
if (val === 'CURRENT_TIMESTAMP' || val === 'now()') {
209201
return (ab) => ab.InvocationExpr.setFunction(getFunctionRef('now', services));
@@ -447,3 +439,37 @@ WHERE
447439
AND "cls"."relname" !~ '_prisma_migrations'
448440
ORDER BY "ns"."nspname", "cls"."relname" ASC;
449441
`;
442+
443+
function typeCastingConvert({defaultValue, enums, val, services}:{val: string, enums: Enum[], defaultValue:string, services:ZModelServices}): ((builder: ExpressionBuilder) => AstFactory<Expression>) | null {
444+
const [value, type] = val
445+
.replace(/'/g, '')
446+
.split('::')
447+
.map((s) => s.trim()) as [string, string];
448+
switch (type) {
449+
case 'character varying':
450+
case 'uuid':
451+
case 'json':
452+
case 'jsonb':
453+
case 'text':
454+
if (value === 'NULL') return null;
455+
return (ab) => ab.StringLiteral.setValue(value);
456+
case 'real':
457+
return (ab) => ab.NumberLiteral.setValue(value);
458+
default: {
459+
const enumDef = enums.find((e) => getDbName(e, true) === type);
460+
if (!enumDef) {
461+
return (ab) =>
462+
ab.InvocationExpr.setFunction(getFunctionRef('dbgenerated', services)).addArg((a) =>
463+
a.setValue((v) => v.StringLiteral.setValue(val)),
464+
);
465+
}
466+
const enumField = enumDef.fields.find((v) => getDbName(v) === value);
467+
if (!enumField) {
468+
throw new Error(
469+
`Enum value ${value} not found in enum ${type} for default value ${defaultValue}`,
470+
);
471+
}
472+
return (ab) => ab.ReferenceExpr.setTarget(enumField);
473+
}
474+
}
475+
}

packages/cli/src/actions/pull/provider/sqlite.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export const sqlite: IntrospectionProvider = {
142142
nulls: null,
143143
})),
144144
};
145-
}).reverse(); // Reverse to maintain creation order
145+
});
146146

147147
// Foreign keys mapping by column name
148148
const fkRows = all<{

0 commit comments

Comments
 (0)