Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/sql/src/modules/lokee-weave/reversal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ describe('parseTypeText', () => {
expect(parseTypeText('numeric(10,2)')).toEqual({ base: 'numeric', precision: 10, scale: 2 });
});

it('reads DECIMAL(p) as precision with scale 0', () => {
expect(parseTypeText('numeric(10)')).toEqual({ base: 'numeric', precision: 10, scale: 0 });
expect(parseTypeText('decimal(8)')).toEqual({ base: 'decimal', precision: 8, scale: 0 });
expect(parseTypeText('NUMBER(19)')).toEqual({ base: 'number', precision: 19, scale: 0 });
});

it('reads an unparameterised type', () => {
expect(parseTypeText('integer')).toEqual({ base: 'integer' });
});
Expand Down Expand Up @@ -149,6 +155,17 @@ describe('classifyReversal — the cases users actually ask about', () => {
expect(verdict.dataLoss).toContain('2 decimal places');
});

it('warns when reverting to DECIMAL(p) from a scaled decimal', () => {
const verdict = classifyReversal(
'column:ORDER.TOTAL',
column({ dataType: 'numeric(10,2)' }),
column({ dataType: 'numeric(10)' })
);
expect(verdict.risk).toBe('lossy');
expect(verdict.summary).toMatch(/scale 2 → 0/);
expect(verdict.dataLoss).toContain('0 decimal places');
});

it('warns when numeric precision is reduced', () => {
const verdict = classifyReversal(
'column:ORDER.TOTAL',
Expand Down
13 changes: 12 additions & 1 deletion packages/sql/src/modules/lokee-weave/reversal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,18 @@ export function parseTypeText(text: string | null | undefined): ParsedType | nul
.map((a) => a.trim());
const nums = args.map((a) => (/^\d+$/.test(a) ? Number(a) : undefined));
// `varchar(max)` carries no usable bound — treat as unbounded.
if (args.length === 1) return { base, length: nums[0] };
if (args.length === 1) {
// SQL / Oracle: DECIMAL(p) / NUMERIC(p) / NUMBER(p) ≡ (p,0). Storing the
// sole arg as `length` made scale comparisons skip DECIMAL(10,2) →
// DECIMAL(10) and classify a truncating revert as safe.
if (
(base === 'numeric' || base === 'decimal' || base === 'number' || base === 'dec') &&
nums[0] !== undefined
) {
return { base, precision: nums[0], scale: 0 };
}
return { base, length: nums[0] };
}
return { base, precision: nums[0], scale: nums[1] };
}

Expand Down
43 changes: 43 additions & 0 deletions packages/sql/src/modules/migration-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,49 @@ describe('findNarrowingTypeChanges', () => {
expect(findNarrowingTypeChanges(tables, { ORDERS: true }, postgresSqlDialect)).toHaveLength(1);
});

it('flags decimal scale decrease', () => {
const tables = [
diff({
tableName: 'ORDERS',
objectType: 'TABLE',
status: 'MODIFIED',
columnDiffs: [
{
name: 'AMOUNT',
status: 'MODIFIED',
source: { type: 'numeric(10,0)', nullable: true },
target: { type: 'numeric(10,2)', nullable: true },
},
],
}),
];
expect(findNarrowingTypeChanges(tables, { ORDERS: true }, postgresSqlDialect)).toHaveLength(1);
});

it('flags DECIMAL(p) as scale-0 narrowing (SQL: DECIMAL(p) ≡ DECIMAL(p,0))', () => {
// Omitted scale used to skip the check entirely — migrate truncated cents
// with no NARROWING_TYPE_CHANGE warning.
const tables = [
diff({
tableName: 'ORDERS',
objectType: 'TABLE',
status: 'MODIFIED',
columnDiffs: [
{
name: 'AMOUNT',
status: 'MODIFIED',
source: { type: 'numeric(10)', nullable: true },
target: { type: 'numeric(10,2)', nullable: true },
},
],
}),
];
const issues = findNarrowingTypeChanges(tables, { ORDERS: true }, postgresSqlDialect);
expect(issues).toHaveLength(1);
expect(issues[0]).toMatchObject({ code: 'NARROWING_TYPE_CHANGE', tableName: 'ORDERS' });
expect(issues[0]!.message).toMatch(/numeric\(10,2\).*numeric\(10\)/);
});

it('does not flag a widening change', () => {
const tables = [
diff({
Expand Down
7 changes: 6 additions & 1 deletion packages/sql/src/modules/migration-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,12 @@ function isNarrowing(source: CanonicalType, target: CanonicalType): boolean {
if (source.length !== undefined && target.length !== undefined && target.length < source.length) return true;
if (source.base === 'decimal') {
if (source.precision !== undefined && target.precision !== undefined && target.precision < source.precision) return true;
if (source.scale !== undefined && target.scale !== undefined && target.scale < source.scale) return true;
// SQL: DECIMAL(p) ≡ DECIMAL(p,0). A missing scale on a sized decimal is
// zero, not "unknown" — treating it as unknown skipped DECIMAL(10,2) →
// DECIMAL(10) and let migrates truncate fractional digits unwarned.
const sourceScale = source.precision !== undefined ? (source.scale ?? 0) : undefined;
const targetScale = target.precision !== undefined ? (target.scale ?? 0) : undefined;
if (sourceScale !== undefined && targetScale !== undefined && targetScale < sourceScale) return true;
}
return false;
}
Expand Down
Loading