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

Commit 39a04eb

Browse files
committed
fix(cli): improve db pull for relations and defaults
Prevents field name collisions during introspection by refining the naming strategy for self-referencing relations with multiple foreign keys. Extends support for JSON and Bytes default values across MySQL, PostgreSQL, and SQLite providers to ensure consistent schema restoration. Adds test cases for self-referencing models to verify the avoidance of duplicate fields.
1 parent 5c932f2 commit 39a04eb

5 files changed

Lines changed: 76 additions & 1 deletion

File tree

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,13 +533,20 @@ export function syncRelation({
533533
sourceModel.fields.splice(firstSourceFieldId, 0, sourceFieldFactory.node); // Insert the relation field before the first FK scalar field
534534

535535
const oppositeFieldPrefix = /[0-9]/g.test(targetModel.name.charAt(0)) ? '_' : '';
536-
const { name: oppositeFieldName } = resolveNameCasing(
536+
let { name: oppositeFieldName } = resolveNameCasing(
537537
options.fieldCasing,
538538
similarRelations > 0
539539
? `${oppositeFieldPrefix}${lowerCaseFirst(sourceModel.name)}_${firstColumn}`
540540
: `${lowerCaseFirst(resolveNameCasing(options.fieldCasing, sourceModel.name).name)}${relation.references.type === 'many'? 's' : ''}`,
541541
);
542542

543+
if (targetModel.fields.find((f) => f.name === oppositeFieldName)) {
544+
({ name: oppositeFieldName } = resolveNameCasing(
545+
options.fieldCasing,
546+
`${lowerCaseFirst(sourceModel.name)}_${firstColumn}To${relation.references.table}_${relation.references.columns[0]}`,
547+
));
548+
}
549+
543550
const targetFieldFactory = new DataFieldFactory()
544551
.setContainer(targetModel)
545552
.setName(oppositeFieldName)

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,10 @@ export const mysql: IntrospectionProvider = {
266266
return (ab) => ab.InvocationExpr.setFunction(getFunctionRef('uuid', services));
267267
}
268268
return (ab) => ab.StringLiteral.setValue(val);
269+
case 'Json':
270+
return (ab) => ab.StringLiteral.setValue(val);
271+
case 'Bytes':
272+
return (ab) => ab.StringLiteral.setValue(val);
269273
}
270274

271275
// Handle function calls (e.g., uuid(), now())

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,16 @@ export const postgresql: IntrospectionProvider = {
284284
return (ab) => ab.StringLiteral.setValue(val.slice(1, -1).replace(/''/g, "'"));
285285
}
286286
return (ab) => ab.StringLiteral.setValue(val);
287+
case 'Json':
288+
if (val.includes('::')) {
289+
return typeCastingConvert({defaultValue,enums,val,services});
290+
}
291+
return (ab) => ab.StringLiteral.setValue(val);
292+
case 'Bytes':
293+
if (val.includes('::')) {
294+
return typeCastingConvert({defaultValue,enums,val,services});
295+
}
296+
return (ab) => ab.StringLiteral.setValue(val);
287297
}
288298

289299
if (val.includes('(') && val.includes(')')) {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,10 @@ export const sqlite: IntrospectionProvider = {
394394
return (ab) => ab.StringLiteral.setValue(strippedName);
395395
}
396396
return (ab) => ab.StringLiteral.setValue(val);
397+
case 'Json':
398+
return (ab) => ab.StringLiteral.setValue(val);
399+
case 'Bytes':
400+
return (ab) => ab.StringLiteral.setValue(val);
397401
}
398402

399403
console.warn(`Unsupported default value type: "${defaultValue}" for field type "${fieldType}". Skipping default value.`);

packages/cli/test/db/pull.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,56 @@ model Tag {
102102
expect(restoredSchema).toEqual(schema);
103103
});
104104

105+
it('should restore self-referencing model with multiple FK columns without duplicate fields', async () => {
106+
const { workDir, schema } = await createProject(
107+
`model Category {
108+
id Int @id @default(autoincrement())
109+
categoryParentId Category? @relation('Category_parentIdToCategory', fields: [parentId], references: [id])
110+
parentId Int?
111+
categoryBuddyId Category? @relation('Category_buddyIdToCategory', fields: [buddyId], references: [id])
112+
buddyId Int?
113+
categoryMentorId Category? @relation('Category_mentorIdToCategory', fields: [mentorId], references: [id])
114+
mentorId Int?
115+
categoryParentIdToCategoryId Category[] @relation('Category_parentIdToCategory')
116+
categoryBuddyIdToCategoryId Category[] @relation('Category_buddyIdToCategory')
117+
categoryMentorIdToCategoryId Category[] @relation('Category_mentorIdToCategory')
118+
}`,
119+
);
120+
runCli('db push', workDir);
121+
122+
const schemaFile = path.join(workDir, 'zenstack/schema.zmodel');
123+
124+
fs.writeFileSync(schemaFile, getDefaultPrelude());
125+
runCli('db pull --indent 4', workDir);
126+
127+
const restoredSchema = getSchema(workDir);
128+
129+
expect(restoredSchema).toEqual(schema);
130+
});
131+
132+
it('should preserve self-referencing model with multiple FK columns', async () => {
133+
const { workDir, schema } = await createProject(
134+
`model Category {
135+
id Int @id @default(autoincrement())
136+
category Category? @relation('Category_parentIdToCategory', fields: [parentId], references: [id])
137+
parentId Int?
138+
buddy Category? @relation('Category_buddyIdToCategory', fields: [buddyId], references: [id])
139+
buddyId Int?
140+
mentor Category? @relation('Category_mentorIdToCategory', fields: [mentorId], references: [id])
141+
mentorId Int?
142+
categories Category[] @relation('Category_parentIdToCategory')
143+
buddys Category[] @relation('Category_buddyIdToCategory')
144+
mentees Category[] @relation('Category_mentorIdToCategory')
145+
}`,
146+
);
147+
runCli('db push', workDir);
148+
runCli('db pull --indent 4', workDir);
149+
150+
const restoredSchema = getSchema(workDir);
151+
152+
expect(restoredSchema).toEqual(schema);
153+
});
154+
105155
it('should restore one-to-one relation when FK is the single-column primary key', async () => {
106156
const { workDir, schema } = await createProject(
107157
`model Profile {

0 commit comments

Comments
 (0)