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
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { canonicalStringify } from '@internal/utils/canonical-stringify';
* Structural equality for two resolved column defaults, ported from the
* relational walk's `columnDefaultsEqual` normalized branch: kinds must
* match; literal values are normalized (Date and temporal-typed strings to
* ISO instants) then compared canonically (JSON objects match their
* canonical string form); function expressions compare case- and
* ISO instants, and a 64-bit-integer native type's safe-integer number to
* its decimal-text spelling) then compared canonically (JSON objects match
* their canonical string form); function expressions compare case- and
* whitespace-insensitively.
*
* `nativeType` provides the temporal-normalization context (the actual
* side's resolved native type in a diff comparison).
* `nativeType` provides the temporal- and int64-normalization context (the
* actual side's resolved native type in a diff comparison).
*/
export function resolvedDefaultsEqual(
expected: ColumnDefault,
Expand Down Expand Up @@ -43,6 +44,12 @@ function isTemporalNativeType(nativeType?: string): boolean {
return normalized.includes('timestamp') || normalized === 'date';
}

function isInt64NativeType(nativeType?: string): boolean {
if (!nativeType) return false;
const normalized = nativeType.toLowerCase();
return normalized === 'int8' || normalized === 'bigint';
}

function normalizeLiteralValue(value: unknown, nativeType?: string): unknown {
if (value instanceof Date) {
return value.toISOString();
Expand All @@ -53,6 +60,9 @@ function normalizeLiteralValue(value: unknown, nativeType?: string): unknown {
return parsed.toISOString();
}
}
if (typeof value === 'number' && Number.isSafeInteger(value) && isInt64NativeType(nativeType)) {
return String(value);
}
return value;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,48 @@ describe('resolvedDefaultsEqual', () => {
);
});
});

describe('int64 literals', () => {
it('matches a safe-integer number against the decimal text it denotes, under int8', () => {
expect({
numberFirst: resolvedDefaultsEqual(literal(0), literal('0'), 'int8'),
textFirst: resolvedDefaultsEqual(literal('0'), literal(0), 'int8'),
}).toEqual({ numberFirst: true, textFirst: true });
});

it('matches a safe-integer number against the decimal text it denotes, under bigint', () => {
expect(resolvedDefaultsEqual(literal(42), literal('42'), 'bigint')).toBe(true);
});

it('matches a negative safe integer against its decimal text', () => {
expect(resolvedDefaultsEqual(literal(-7), literal('-7'), 'int8')).toBe(true);
});

it('fires when the decimal text denotes a different number', () => {
expect(resolvedDefaultsEqual(literal(1), literal('2'), 'int8')).toBe(false);
});

it('leaves a number against its decimal text alone without an int8/bigint native type', () => {
expect(resolvedDefaultsEqual(literal(0), literal('0'), 'int4')).toBe(false);
expect(resolvedDefaultsEqual(literal(0), literal('0'))).toBe(false);
});

it('declines to match a rounded number against the exact decimal text it lost', () => {
expect(
resolvedDefaultsEqual(literal('9007199254740993'), literal(9007199254740992), 'int8'),
).toBe(false);
});

it('declines to match outside the safe-integer range, even when the text is exact', () => {
expect(resolvedDefaultsEqual(literal(1e17), literal('100000000000000000'), 'int8')).toBe(
false,
);
});

it('still compares two decimal-text strings by identity past the safe integer range', () => {
expect(
resolvedDefaultsEqual(literal('9007199254740993'), literal('9007199254740993'), 'int8'),
).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { type Contract, coreHash, profileHash } from '@internal/contract/types';
import { INIT_ADDITIVE_POLICY } from '@internal/family-sql/control';
import { APP_SPACE_ID } from '@internal/framework-components/control';
import { UNBOUND_NAMESPACE_ID } from '@internal/framework-components/ir';
import { SqlStorage } from '@internal/sql-contract/types';
import {
PostgresDatabaseSchemaNode,
postgresCreateNamespace,
} from '@internal/target-postgres/types';
import { applicationDomainOf } from '@repo/test-utils';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import {
controlAdapter,
createDriver,
createTestDatabase,
emptySchema,
familyInstance,
formatRunnerFailure,
frameworkComponents,
type PostgresControlDriver,
postgresTargetDescriptor,
resetDatabase,
synthEdges,
testTimeout,
} from './fixtures/runner-fixtures';

const PAST_SAFE_INTEGER_TEXT = '9007199254740993';

function moneyContract(): Contract<SqlStorage> {
return {
target: 'postgres',
targetFamily: 'sql',
profileHash: profileHash('int8-literal-default'),
storage: new SqlStorage({
storageHash: coreHash('int8-literal-default'),
namespaces: {
[UNBOUND_NAMESPACE_ID]: postgresCreateNamespace({
id: UNBOUND_NAMESPACE_ID,
entries: {
table: {
ps: {
columns: {
id: { nativeType: 'text', codecId: 'pg/text@1', nullable: false },
v: {
nativeType: 'int8',
codecId: 'pg/int8number@1',
nullable: false,
default: { kind: 'literal', value: 0 },
},
w: {
nativeType: 'int4',
codecId: 'pg/int4@1',
nullable: false,
default: { kind: 'literal', value: 0 },
},
bigIntSmall: {
nativeType: 'int8',
codecId: 'pg/int8@1',
nullable: false,
default: { kind: 'literal', value: '0' },
},
bigIntPastSafeInteger: {
nativeType: 'int8',
codecId: 'pg/int8@1',
nullable: false,
default: { kind: 'literal', value: PAST_SAFE_INTEGER_TEXT },
},
},
primaryKey: { columns: ['id'] },
uniques: [],
indexes: [],
foreignKeys: [],
},
},
},
}),
},
}),
roots: {},
domain: applicationDomainOf({ models: {} }),
capabilities: {},
extensions: {},
meta: {},
};
}

describe('Schema verification after runner - int8 literal default (issue #30174)', {
concurrent: false,
}, () => {
let database: Awaited<ReturnType<typeof createTestDatabase>>;
let driver: PostgresControlDriver | undefined;

beforeAll(async () => {
database = await createTestDatabase();
}, testTimeout);

afterAll(async () => {
if (database) {
await database.close();
}
}, testTimeout);

beforeEach(async () => {
driver = await createDriver(database.connectionString);
await resetDatabase(driver);
}, testTimeout);

afterEach(async () => {
if (driver) {
await driver.close();
driver = undefined;
}
}, testTimeout);

it('applies and verifies literal defaults on int8 (BigIntNumber, BigInt) and int4 columns', {
timeout: testTimeout,
}, async () => {
const contract = moneyContract();
const planner = postgresTargetDescriptor.createPlanner(controlAdapter);
const runner = postgresTargetDescriptor.createRunner(familyInstance);

const planResult = planner.plan({
contract,
schema: emptySchema,
policy: INIT_ADDITIVE_POLICY,
fromContract: null,
frameworkComponents,
spaceId: APP_SPACE_ID,
snapshotsImportPath: '../../snapshots',
});
if (planResult.kind !== 'success') {
throw new Error(`Planner failed: ${planResult.kind}`);
}

const executeResult = await runner.execute({
driver: driver!,
perSpaceOptions: [
{
space: planResult.plan.spaceId ?? APP_SPACE_ID,
plan: planResult.plan,
migrationEdges: synthEdges(planResult.plan),
driver: driver!,
destinationContract: contract,
policy: INIT_ADDITIVE_POLICY,
frameworkComponents,
},
],
});

if (!executeResult.ok) {
throw new Error(
`db migrate failed on its own post-apply verification:\n${formatRunnerFailure(executeResult.failure)}`,
);
}

const schema = await familyInstance.introspect({ driver: driver!, contract });
const verifyResult = familyInstance.verifySchema({
contract,
schema,
strict: false,
frameworkComponents,
});

expect(verifyResult.ok).toBe(true);
expect(verifyResult.schema.issues).toHaveLength(0);

PostgresDatabaseSchemaNode.assert(schema);
const column = schema.namespaces['public']?.tables['ps']?.columns['bigIntPastSafeInteger'];
expect(column?.resolvedDefault).toEqual({ kind: 'literal', value: PAST_SAFE_INTEGER_TEXT });
});
});
Loading