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
36 changes: 34 additions & 2 deletions packages/sql/src/modules/type-mapping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('cross-dialect type translation', () => {
expect(xlate(db2, pg, 'INTEGER').sql).toBe('integer');
expect(xlate(db2, pg, 'DECIMAL(10,2)').sql).toBe('numeric(10,2)');
expect(xlate(db2, pg, 'CLOB').sql).toBe('text');
expect(xlate(db2, pg, 'TIMESTAMP').sql).toBe('timestamp');
expect(xlate(db2, pg, 'TIMESTAMP').sql).toBe('timestamp(6)');
// DBCLOB has no direct Postgres type → mapped to text (no warning needed, text covers it)
expect(xlate(db2, pg, 'DBCLOB(1048576)').sql).toBe('text');
});
Expand All @@ -49,7 +49,8 @@ describe('cross-dialect type translation', () => {
it('SQL Server → Postgres', () => {
expect(xlate(mssql, pg, 'bit').sql).toBe('boolean');
expect(xlate(mssql, pg, 'nvarchar(max)').sql).toBe('text');
expect(xlate(mssql, pg, 'datetime2').sql).toBe('timestamp');
// Bare datetime2 ≡ datetime2(7) in SQL Server.
expect(xlate(mssql, pg, 'datetime2').sql).toBe('timestamp(7)');
expect(xlate(mssql, pg, 'uniqueidentifier').sql).toBe('uuid');
});

Expand Down Expand Up @@ -96,10 +97,41 @@ describe('cross-dialect type translation', () => {
expect(xlate(pg, oracle, 'timestamp(6)').sql).toBe('TIMESTAMP(6)');
});

it('fills dialect-default fsp when catalogs omit the parenthetical', () => {
// Postgres format_type omits (6) for the default typmod; Oracle TIMESTAMP
// defaults to TIMESTAMP(6). Without attaching that length, MySQL rendered
// bare datetime (= fsp 0) and silently dropped microseconds on migrate.
expect(pg.parseType('timestamp without time zone')).toMatchObject({
base: 'timestamp',
length: 6,
});
expect(pg.parseType('timestamp with time zone')).toMatchObject({
base: 'timestamptz',
length: 6,
});
expect(pg.parseType('time without time zone')).toMatchObject({ base: 'time', length: 6 });
expect(xlate(pg, mysql, 'timestamp without time zone').sql).toBe('datetime(6)');
expect(xlate(pg, mysql, 'time').sql).toBe('time(6)');
expect(oracle.parseType('TIMESTAMP')).toMatchObject({ base: 'timestamp', length: 6 });
expect(xlate(oracle, mysql, 'TIMESTAMP').sql).toBe('datetime(6)');
// SQL Server datetime ≈ ms; smalldatetime has no fractional seconds.
expect(mssql.parseType('datetime')).toMatchObject({ base: 'timestamp', length: 3 });
expect(mssql.parseType('smalldatetime')).toMatchObject({ base: 'timestamp', length: 0 });
expect(xlate(mssql, mysql, 'datetime').sql).toBe('datetime(3)');
expect(xlate(mssql, mysql, 'smalldatetime').sql).toBe('datetime(0)');
expect(mssql.parseType('datetime2')).toMatchObject({ base: 'timestamp', length: 7 });
// MySQL bare datetime/time really are fsp 0 — do not invent a default.
expect(mysql.parseType('datetime')).toMatchObject({ base: 'timestamp' });
expect(mysql.parseType('datetime').length).toBeUndefined();
expect(xlate(mysql, mysql, 'datetime').sql).toBe('datetime');
});

it('distinguishes temporal fsp in canonicalEquals', () => {
expect(canonicalEquals(mysql.parseType('datetime(6)'), mysql.parseType('datetime'))).toBe(false);
expect(canonicalEquals(pg.parseType('timestamp(6)'), pg.parseType('timestamp(3)'))).toBe(false);
expect(canonicalEquals(pg.parseType('timestamp(6)'), pg.parseType('timestamp(6)'))).toBe(true);
// Bare Postgres timestamp ≡ timestamp(6).
expect(canonicalEquals(pg.parseType('timestamp'), pg.parseType('timestamp(6)'))).toBe(true);
});

it('attaches a warning when the target has no exact equivalent', () => {
Expand Down
14 changes: 14 additions & 0 deletions packages/sql/src/modules/type-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,20 @@ export const decimalAs = (kw: string): RenderRule => (t) =>
/** Always renders `sql`, attaching a warning (for known inexact mappings). */
export const warn = (sql: string, message: string): RenderRule => () => ({ sql, warning: message });

/**
* Catalogs often omit the parenthetical when a temporal type uses its dialect
* default fractional-seconds precision (Postgres `timestamp` ≡ timestamp(6),
* Oracle `TIMESTAMP` ≡ TIMESTAMP(6)). Without that length, MySQL/MariaDB render
* bare `datetime`/`time` (= fsp 0) and silently truncate sub-seconds on migrate.
*/
export function withDefaultTemporalFsp(parsed: CanonicalType, defaultFsp: number): CanonicalType {
if (parsed.length !== undefined) return parsed;
if (parsed.base !== 'time' && parsed.base !== 'timestamp' && parsed.base !== 'timestamptz') {
return parsed;
}
return { ...parsed, length: defaultFsp };
}

/** True when two canonical types are equivalent (base + relevant size fields). */
export function canonicalEquals(a: CanonicalType, b: CanonicalType): boolean {
if (a.base !== b.base) return false;
Expand Down
19 changes: 18 additions & 1 deletion packages/sql/src/providers/db2/db2.sql-dialect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import type { SqlDialect, ColumnSpec } from '../../modules/sql-dialect.interface.js';
import { makeDialectTypeFns, plain, sized, sizedOr, decimalAs, temporalAs, warn } from '../../modules/type-mapping.js';
import {
makeDialectTypeFns,
plain,
sized,
sizedOr,
decimalAs,
temporalAs,
warn,
withDefaultTemporalFsp,
} from '../../modules/type-mapping.js';

const types = makeDialectTypeFns({
label: 'Db2',
Expand Down Expand Up @@ -206,4 +215,12 @@ export const db2SqlDialect: SqlDialect = {
},

...types,
// Bare TIMESTAMP ≡ TIMESTAMP(6). TIME has no fractional seconds on Db2.
parseType: (raw: string) => {
const parsed = types.parseType(raw);
if (parsed.base === 'timestamp' && parsed.length === undefined) {
return withDefaultTemporalFsp(parsed, 6);
}
return parsed;
},
};
10 changes: 9 additions & 1 deletion packages/sql/src/providers/duckDb/duckdb.sql-dialect.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import type { SqlDialect, ColumnSpec } from '../../modules/sql-dialect.interface.js';
import type { TableSchema } from '../../interfaces/index.js';
import { makeDialectTypeFns, plain, sized, decimalAs } from '../../modules/type-mapping.js';
import {
makeDialectTypeFns,
plain,
sized,
decimalAs,
withDefaultTemporalFsp,
} from '../../modules/type-mapping.js';

// DuckDB's type system is Postgres-flavored. Key differences from Postgres:
// binary is BLOB (not bytea), and it exposes HUGEINT / unsigned ints (mapped
Expand Down Expand Up @@ -142,4 +148,6 @@ export const duckDbSqlDialect: SqlDialect = {
},

...types,
// DuckDB TIMESTAMP defaults to microsecond precision when scale is omitted.
parseType: (raw: string) => withDefaultTemporalFsp(types.parseType(raw), 6),
};
13 changes: 12 additions & 1 deletion packages/sql/src/providers/oracle/oracle.sql-dialect.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import type { SqlDialect, ColumnSpec } from '../../modules/sql-dialect.interface.js';
import type { TableSchema } from '../../interfaces/index.js';
import { makeDialectTypeFns, plain, sized, sizedOr, decimalAs, temporalAs, warn } from '../../modules/type-mapping.js';
import {
makeDialectTypeFns,
plain,
sized,
sizedOr,
decimalAs,
temporalAs,
warn,
withDefaultTemporalFsp,
} from '../../modules/type-mapping.js';

const types = makeDialectTypeFns({
label: 'Oracle',
Expand Down Expand Up @@ -205,4 +214,6 @@ export const oracleSqlDialect: SqlDialect = {
},

...types,
// Bare TIMESTAMP ≡ TIMESTAMP(6). Catalogs sometimes omit DATA_SCALE.
parseType: (raw: string) => withDefaultTemporalFsp(types.parseType(raw), 6),
};
18 changes: 17 additions & 1 deletion packages/sql/src/providers/postgres/postgres.sql-dialect.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import type { SqlDialect, ColumnSpec } from '../../modules/sql-dialect.interface.js';
import type { TableSchema } from '../../interfaces/index.js';
import { makeDialectTypeFns, plain, sized, decimalAs, temporalAs } from '../../modules/type-mapping.js';
import {
makeDialectTypeFns,
plain,
sized,
decimalAs,
temporalAs,
withDefaultTemporalFsp,
} from '../../modules/type-mapping.js';

const types = makeDialectTypeFns({
label: 'PostgreSQL',
Expand Down Expand Up @@ -68,6 +75,14 @@ function viewDepTag(qualifiedTable: string): string {
return qualifiedTable.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 48);
}

/**
* Postgres `format_type` omits `(6)` when typmod is the default (-1). Bare
* `timestamp` / `timestamptz` / `time` still store 6 fractional digits.
*/
function parsePostgresType(raw: string) {
return withDefaultTemporalFsp(types.parseType(raw), 6);
}

export const postgresSqlDialect: SqlDialect = {
identityClause(c: ColumnSpec): string {
return c.identity ? ` GENERATED ${c.identityGeneration ?? 'ALWAYS'} AS IDENTITY` : '';
Expand Down Expand Up @@ -220,4 +235,5 @@ export const postgresSqlDialect: SqlDialect = {
},

...types,
parseType: parsePostgresType,
};
10 changes: 9 additions & 1 deletion packages/sql/src/providers/redshift/redshift.sql-dialect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import type { SqlDialect, ColumnSpec } from '../../modules/sql-dialect.interface.js';
import { makeDialectTypeFns, plain, sized, decimalAs } from '../../modules/type-mapping.js';
import {
makeDialectTypeFns,
plain,
sized,
decimalAs,
withDefaultTemporalFsp,
} from '../../modules/type-mapping.js';

// Redshift type system is a subset of Postgres. information_schema returns
// standard SQL type names, so the mapping mirrors the Postgres dialect.
Expand Down Expand Up @@ -117,4 +123,6 @@ export const redshiftSqlDialect: SqlDialect = {
dropRoutineSignature: true,

...types,
// Same as Postgres: bare timestamp/time still carry microsecond precision.
parseType: (raw: string) => withDefaultTemporalFsp(types.parseType(raw), 6),
};
20 changes: 16 additions & 4 deletions packages/sql/src/providers/sqlServer/sqlserver.sql-dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,25 @@ const types = makeDialectTypeFns({
},
});

/** Attach the fixed MONEY/SMALLMONEY precision the bare parse map cannot express. */
/**
* Attach sizes the bare parse map cannot express:
* - MONEY / SMALLMONEY fixed decimal precision
* - datetime (~ms) / smalldatetime (minutes) / default datetime2|time|datetimeoffset(7)
* when the catalog omits the scale parenthetical
*/
function parseSqlServerType(raw: string) {
const parsed = types.parseType(raw);
if (parsed.base !== 'decimal' || parsed.precision !== undefined) return parsed;
const name = tokenizeType(raw).name;
if (name === 'money') return { ...parsed, precision: 19, scale: 4 };
if (name === 'smallmoney') return { ...parsed, precision: 10, scale: 4 };
if (parsed.base === 'decimal' && parsed.precision === undefined) {
if (name === 'money') return { ...parsed, precision: 19, scale: 4 };
if (name === 'smallmoney') return { ...parsed, precision: 10, scale: 4 };
}
if (parsed.length !== undefined) return parsed;
if (name === 'datetime') return { ...parsed, length: 3 };
if (name === 'smalldatetime') return { ...parsed, length: 0 };
if (name === 'datetime2' || name === 'time' || name === 'datetimeoffset') {
return { ...parsed, length: 7 };
}
return parsed;
}

Expand Down
Loading