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

Commit c2cf38f

Browse files
committed
fix: address PR comments
1 parent 4212b3e commit c2cf38f

7 files changed

Lines changed: 124 additions & 40 deletions

File tree

packages/cli/src/actions/db.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ async function runPull(options: PullOptions) {
8989
const treatAsFile =
9090
!!outPath &&
9191
((fs.existsSync(outPath) && fs.lstatSync(outPath).isFile()) || path.extname(outPath) !== '');
92-
92+
9393
const { model, services } = await loadSchemaDocument(schemaFile, {
9494
returnServices: true,
9595
mergeImports: treatAsFile,
@@ -328,7 +328,7 @@ async function runPull(options: PullOptions) {
328328
return;
329329
}
330330
const originalField = originalFields.at(0);
331-
331+
332332
if (!originalField) {
333333
getModelChanges(originalDataModel.name).addedFields.push(colors.green(`+ ${f.name}`));
334334
(f as any).$container = originalDataModel;
@@ -368,6 +368,9 @@ async function runPull(options: PullOptions) {
368368
!['@map', '@@map', '@default', '@updatedAt'].includes(attr.decl.$refText),
369369
)
370370
.forEach((attr) => {
371+
// attach the new attribute to the original field
372+
const cloned = { ...attr, $container: originalField } as typeof attr;
373+
originalField.attributes.push(cloned);
371374
getModelChanges(originalDataModel.name).addedAttributes.push(
372375
colors.green(`+ ${attr.decl.$refText} to field: ${originalDataModel.name}.${f.name}`),
373376
);

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,9 @@ export function syncTable({
213213
}
214214
table.columns.forEach((column) => {
215215
if (column.foreign_key_table) {
216+
// Check if this FK column is the table's single-column primary key
217+
// If so, it should be treated as a one-to-one relation
218+
const isSingleColumnPk = !multiPk && column.pk;
216219
relations.push({
217220
schema: table.schema,
218221
table: table.name,
@@ -226,7 +229,7 @@ export function syncTable({
226229
schema: column.foreign_key_schema,
227230
table: column.foreign_key_table,
228231
column: column.foreign_key_column,
229-
type: column.unique ? 'one' : 'many',
232+
type: column.unique || isSingleColumnPk ? 'one' : 'many',
230233
},
231234
});
232235
}

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

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -228,24 +228,34 @@ export const mysql: IntrospectionProvider = {
228228
return (ab) => ab.NumberLiteral.setValue(val);
229229

230230
case 'Float':
231-
if (/^-?\d+\.\d+$/.test(val)) {
232-
const numVal = parseFloat(val);
233-
return (ab) => ab.NumberLiteral.setValue(numVal === Math.floor(numVal) ? numVal.toFixed(1) : String(numVal));
234-
}
235-
if (/^-?\d+$/.test(val)) {
236-
return (ab) => ab.NumberLiteral.setValue(val + '.0');
237-
}
238-
return (ab) => ab.NumberLiteral.setValue(val);
231+
// Integer strings: append '.0'
232+
if (/^-?\d+$/.test(val)) {
233+
return (ab) => ab.NumberLiteral.setValue(val + '.0');
234+
}
235+
// Decimal strings: preserve exactly to avoid parseFloat precision loss
236+
if (/^-?\d+\.\d+$/.test(val)) {
237+
return (ab) => ab.NumberLiteral.setValue(val);
238+
}
239+
// Other values: return unchanged
240+
return (ab) => ab.NumberLiteral.setValue(val);
239241

240242
case 'Decimal':
241-
if (/^-?\d+\.\d+$/.test(val)) {
242-
const numVal = parseFloat(val);
243-
return (ab) => ab.NumberLiteral.setValue(numVal === Math.floor(numVal) ? numVal.toFixed(2) : String(numVal));
244-
}
245-
if (/^-?\d+$/.test(val)) {
246-
return (ab) => ab.NumberLiteral.setValue(val + '.00');
247-
}
248-
return (ab) => ab.NumberLiteral.setValue(val);
243+
// Integer strings: append '.00'
244+
if (/^-?\d+$/.test(val)) {
245+
return (ab) => ab.NumberLiteral.setValue(val + '.00');
246+
}
247+
// Decimal strings: normalize to minimum 2 decimal places, strip excess trailing zeros
248+
if (/^-?\d+\.\d+$/.test(val)) {
249+
const [integerPart, fractionalPart] = val.split('.');
250+
// Strip trailing zeros, but keep at least 2 digits
251+
let normalized = fractionalPart!.replace(/0+$/, '');
252+
if (normalized.length < 2) {
253+
normalized = normalized.padEnd(2, '0');
254+
}
255+
return (ab) => ab.NumberLiteral.setValue(`${integerPart}.${normalized}`);
256+
}
257+
// Other values: return unchanged
258+
return (ab) => ab.NumberLiteral.setValue(val);
249259

250260
case 'Boolean':
251261
return (ab) => ab.BooleanLiteral.setValue(val.toLowerCase() === 'true' || val === '1' || val === "b'1'");

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -162,27 +162,37 @@ export const postgresql: IntrospectionProvider = {
162162
return typeCastingConvert({defaultValue,enums,val,services});
163163
}
164164

165-
if (/^-?\d+\.\d+$/.test(val)) {
166-
const numVal = parseFloat(val);
167-
return (ab) => ab.NumberLiteral.setValue(numVal === Math.floor(numVal) ? numVal.toFixed(1) : String(numVal));
168-
}
165+
// Integer strings: append '.0'
169166
if (/^-?\d+$/.test(val)) {
170167
return (ab) => ab.NumberLiteral.setValue(val + '.0');
171168
}
169+
// Decimal strings: preserve exactly to avoid parseFloat precision loss
170+
if (/^-?\d+\.\d+$/.test(val)) {
171+
return (ab) => ab.NumberLiteral.setValue(val);
172+
}
173+
// Other values: return unchanged
172174
return (ab) => ab.NumberLiteral.setValue(val);
173175

174176
case 'Decimal':
175177
if (val.includes('::')) {
176178
return typeCastingConvert({defaultValue,enums,val,services});
177179
}
178180

179-
if (/^-?\d+\.\d+$/.test(val)) {
180-
const numVal = parseFloat(val);
181-
return (ab) => ab.NumberLiteral.setValue(numVal === Math.floor(numVal) ? numVal.toFixed(2) : String(numVal));
182-
}
181+
// Integer strings: append '.00'
183182
if (/^-?\d+$/.test(val)) {
184183
return (ab) => ab.NumberLiteral.setValue(val + '.00');
185184
}
185+
// Decimal strings: normalize to minimum 2 decimal places, strip excess trailing zeros
186+
if (/^-?\d+\.\d+$/.test(val)) {
187+
const [integerPart, fractionalPart] = val.split('.');
188+
// Strip trailing zeros, but keep at least 2 digits
189+
let normalized = fractionalPart!.replace(/0+$/, '');
190+
if (normalized.length < 2) {
191+
normalized = normalized.padEnd(2, '0');
192+
}
193+
return (ab) => ab.NumberLiteral.setValue(`${integerPart}.${normalized}`);
194+
}
195+
// Other values: return unchanged
186196
return (ab) => ab.NumberLiteral.setValue(val);
187197

188198
case 'Boolean':

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -329,23 +329,33 @@ export const sqlite: IntrospectionProvider = {
329329
return (ab) => ab.NumberLiteral.setValue(val);
330330

331331
case 'Float':
332-
if (/^-?\d+\.\d+$/.test(val)) {
333-
const numVal = parseFloat(val);
334-
return (ab) => ab.NumberLiteral.setValue(numVal === Math.floor(numVal) ? numVal.toFixed(1) : String(numVal));
335-
}
332+
// Integer strings: append '.0'
336333
if (/^-?\d+$/.test(val)) {
337334
return (ab) => ab.NumberLiteral.setValue(val + '.0');
338335
}
336+
// Decimal strings: preserve exactly to avoid parseFloat precision loss
337+
if (/^-?\d+\.\d+$/.test(val)) {
338+
return (ab) => ab.NumberLiteral.setValue(val);
339+
}
340+
// Other values: return unchanged
339341
return (ab) => ab.NumberLiteral.setValue(val);
340342

341343
case 'Decimal':
342-
if (/^-?\d+\.\d+$/.test(val)) {
343-
const numVal = parseFloat(val);
344-
return (ab) => ab.NumberLiteral.setValue(numVal === Math.floor(numVal) ? numVal.toFixed(2) : String(numVal));
345-
}
344+
// Integer strings: append '.00'
346345
if (/^-?\d+$/.test(val)) {
347346
return (ab) => ab.NumberLiteral.setValue(val + '.00');
348347
}
348+
// Decimal strings: normalize to minimum 2 decimal places, strip excess trailing zeros
349+
if (/^-?\d+\.\d+$/.test(val)) {
350+
const [integerPart, fractionalPart] = val.split('.');
351+
// Strip trailing zeros, but keep at least 2 digits
352+
let normalized = fractionalPart!.replace(/0+$/, '');
353+
if (normalized.length < 2) {
354+
normalized = normalized.padEnd(2, '0');
355+
}
356+
return (ab) => ab.NumberLiteral.setValue(`${integerPart}.${normalized}`);
357+
}
358+
// Other values: return unchanged
349359
return (ab) => ab.NumberLiteral.setValue(val);
350360

351361
case 'Boolean':

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,31 @@ model Tag {
107107
expect(restoredSchema).toEqual(schema);
108108
});
109109

110+
it('should restore one-to-one relation when FK is the single-column primary key', async () => {
111+
const { workDir, schema } = await createFormattedProject(
112+
`model Profile {
113+
user User @relation(fields: [id], references: [id], onDelete: Cascade)
114+
id Int @id @default(autoincrement())
115+
bio String?
116+
}
117+
118+
model User {
119+
id Int @id @default(autoincrement())
120+
email String @unique
121+
profile Profile?
122+
}`,
123+
);
124+
runCli('db push', workDir);
125+
126+
const schemaFile = path.join(workDir, 'zenstack/schema.zmodel');
127+
128+
fs.writeFileSync(schemaFile, getDefaultPrelude());
129+
runCli('db pull --indent 4', workDir);
130+
131+
const restoredSchema = getSchema(workDir);
132+
expect(restoredSchema).toEqual(schema);
133+
});
134+
110135
it('should restore schema with indexes and unique constraints', async () => {
111136
const { workDir, schema } = await createFormattedProject(
112137
`model User {
@@ -155,6 +180,29 @@ model Tag {
155180
expect(restoredSchema).toEqual(schema);
156181
});
157182

183+
it('should preserve Decimal and Float default value precision', async () => {
184+
const { workDir, schema } = await createFormattedProject(
185+
`model Product {
186+
id Int @id @default(autoincrement())
187+
price Decimal @default(99.99)
188+
discount Decimal @default(0.50)
189+
taxRate Decimal @default(7.00)
190+
weight Float @default(1.5)
191+
rating Float @default(4.0)
192+
temperature Float @default(98.6)
193+
}`,
194+
);
195+
runCli('db push', workDir);
196+
197+
const schemaFile = path.join(workDir, 'zenstack/schema.zmodel');
198+
199+
fs.writeFileSync(schemaFile, getDefaultPrelude());
200+
runCli('db pull --indent 4', workDir);
201+
202+
const restoredSchema = getSchema(workDir);
203+
expect(restoredSchema).toEqual(schema);
204+
});
205+
158206
});
159207

160208
describe('Pull with existing schema - preserve schema features', () => {

packages/language/src/factory/declaration.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export class DataModelFactory extends AstFactory<DataModel> {
8080
}
8181

8282
addAttribute(builder: (attr: DataModelAttributeFactory) => DataModelAttributeFactory) {
83-
this.attributes.push(builder(new DataModelAttributeFactory()));
83+
this.attributes.push(builder(new DataModelAttributeFactory()).setContainer(this.node));
8484
this.update({
8585
attributes: this.attributes,
8686
});
@@ -104,7 +104,7 @@ export class DataModelFactory extends AstFactory<DataModel> {
104104
}
105105

106106
addField(builder: (field: DataFieldFactory) => DataFieldFactory) {
107-
this.fields.push(builder(new DataFieldFactory()));
107+
this.fields.push(builder(new DataFieldFactory()).setContainer(this.node));
108108
this.update({
109109
fields: this.fields,
110110
});
@@ -306,15 +306,15 @@ export class EnumFactory extends AstFactory<Enum> {
306306
}
307307

308308
addField(builder: (b: EnumFieldFactory) => EnumFieldFactory) {
309-
this.fields.push(builder(new EnumFieldFactory()));
309+
this.fields.push(builder(new EnumFieldFactory()).setContainer(this.node));
310310
this.update({
311311
fields: this.fields,
312312
});
313313
return this;
314314
}
315315

316316
addAttribute(builder: (b: DataModelAttributeFactory) => DataModelAttributeFactory) {
317-
this.attributes.push(builder(new DataModelAttributeFactory()));
317+
this.attributes.push(builder(new DataModelAttributeFactory()).setContainer(this.node));
318318
this.update({
319319
attributes: this.attributes,
320320
});
@@ -348,7 +348,7 @@ export class EnumFieldFactory extends AstFactory<EnumField> {
348348
}
349349

350350
addAttribute(builder: (b: DataFieldAttributeFactory) => DataFieldAttributeFactory) {
351-
this.attributes.push(builder(new DataFieldAttributeFactory()));
351+
this.attributes.push(builder(new DataFieldAttributeFactory()).setContainer(this.node));
352352
this.update({
353353
attributes: this.attributes,
354354
});

0 commit comments

Comments
 (0)