From 6eae4b0fc3f1a941ed40431a84c5efedafdb0905 Mon Sep 17 00:00:00 2001 From: Guy Ben-Aharon Date: Tue, 23 Dec 2025 16:00:07 +0200 Subject: [PATCH] fix: serial ddl import in pg (#1033) * fix: serial ddl import in pg * fix --- .../data/data-types/postgres-data-types.ts | 1 + .../sql-import/__tests__/sql-import.test.ts | 4 +- src/lib/data/sql-import/common.ts | 30 + .../postgresql-alter-add-column.test.ts | 18 +- .../postgresql-alter-column-type.test.ts | 22 +- .../__tests__/postgresql-parser.test.ts | 10 +- .../test-activities-table-import.test.ts | 72 +-- .../test-spell-books-junction-table.test.ts | 4 +- .../postgresql/postgresql.ts | 517 +++++++++--------- 9 files changed, 353 insertions(+), 325 deletions(-) diff --git a/src/lib/data/data-types/postgres-data-types.ts b/src/lib/data/data-types/postgres-data-types.ts index 3c24826f..1ef18d7d 100644 --- a/src/lib/data/data-types/postgres-data-types.ts +++ b/src/lib/data/data-types/postgres-data-types.ts @@ -165,6 +165,7 @@ const synonymMap: Record = { // Timestamp types 'timestamp with time zone': 'timestamptz', 'timestamp without time zone': 'timestamp', + datetime: 'timestamp', // Time types 'time with time zone': 'timetz', diff --git a/src/lib/data/sql-import/__tests__/sql-import.test.ts b/src/lib/data/sql-import/__tests__/sql-import.test.ts index 73fabaf5..5a527331 100644 --- a/src/lib/data/sql-import/__tests__/sql-import.test.ts +++ b/src/lib/data/sql-import/__tests__/sql-import.test.ts @@ -382,10 +382,10 @@ CREATE TABLE playlists ( expect(fieldNames).toContain('items_count'); expect(fieldNames).toContain('units_sold'); - // Verify primary key + // Verify primary key - serial is preserved (not converted to int) const pkField = fields?.find((f) => f.name === 'order_id'); expect(pkField?.primaryKey).toBe(true); - expect(pkField?.type.name).toBe('int'); + expect(pkField?.type.name).toBe('serial'); // Verify decimal fields (decimal is normalized to numeric in PostgreSQL) const totalAmountField = fields?.find((f) => f.name === 'total_amount'); diff --git a/src/lib/data/sql-import/common.ts b/src/lib/data/sql-import/common.ts index 4beaa344..b0b03703 100644 --- a/src/lib/data/sql-import/common.ts +++ b/src/lib/data/sql-import/common.ts @@ -423,6 +423,10 @@ export const typeAffinity: Record> = { int2: 'smallint', bigint: 'bigint', int8: 'bigint', + // Serial types - map to themselves (they're valid PostgreSQL types) + serial: 'serial', + smallserial: 'smallserial', + bigserial: 'bigserial', decimal: 'decimal', numeric: 'numeric', real: 'real', @@ -706,6 +710,32 @@ export function convertToChartDBDiagram( name: column.type, }; } + // Handle PostgreSQL-specific types (not in genericDataTypes) + else if ( + sourceDatabaseType === DatabaseType.POSTGRESQL && + targetDatabaseType === DatabaseType.POSTGRESQL + ) { + const normalizedType = column.type.toLowerCase(); + + // Preserve PostgreSQL-specific types that don't exist in genericDataTypes + // Serial types are PostgreSQL-specific syntax (not true data types) + if ( + normalizedType === 'serial' || + normalizedType === 'smallserial' || + normalizedType === 'bigserial' || + normalizedType === 'jsonb' || + normalizedType === 'timestamptz' || + normalizedType === 'timetz' + ) { + mappedType = { id: normalizedType, name: normalizedType }; + } else { + // Use the standard mapping for other types + mappedType = mapSQLTypeToGenericType( + column.type, + sourceDatabaseType + ); + } + } // Handle SQL Server types specifically else if ( sourceDatabaseType === DatabaseType.SQL_SERVER && diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-alter-add-column.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-alter-add-column.test.ts index 2296f89c..5a707322 100644 --- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-alter-add-column.test.ts +++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-alter-add-column.test.ts @@ -43,7 +43,7 @@ describe('PostgreSQL ALTER TABLE ADD COLUMN Tests', () => { // Check that the id column is present const idColumn = locationTable.columns.find((col) => col.name === 'id'); expect(idColumn).toBeDefined(); - expect(idColumn?.type).toBe('BIGINT'); + expect(idColumn?.type).toBe('bigint'); expect(idColumn?.primaryKey).toBe(true); // Check some of the added columns @@ -51,19 +51,19 @@ describe('PostgreSQL ALTER TABLE ADD COLUMN Tests', () => { (col) => col.name === 'country_id' ); expect(countryIdColumn).toBeDefined(); - expect(countryIdColumn?.type).toBe('INTEGER'); + expect(countryIdColumn?.type).toBe('integer'); const streetColumn = locationTable.columns.find( (col) => col.name === 'street' ); expect(streetColumn).toBeDefined(); - expect(streetColumn?.type).toBe('TEXT'); + expect(streetColumn?.type).toBe('text'); const remarksColumn = locationTable.columns.find( (col) => col.name === 'remarks' ); expect(remarksColumn).toBeDefined(); - expect(remarksColumn?.type).toBe('TEXT'); + expect(remarksColumn?.type).toBe('text'); }); it('should handle ALTER TABLE ADD COLUMN with schema qualification', async () => { @@ -87,13 +87,13 @@ describe('PostgreSQL ALTER TABLE ADD COLUMN Tests', () => { (col) => col.name === 'email' ); expect(emailColumn).toBeDefined(); - expect(emailColumn?.type).toBe('VARCHAR(255)'); + expect(emailColumn?.type).toBe('varchar(255)'); const createdAtColumn = usersTable.columns.find( (col) => col.name === 'created_at' ); expect(createdAtColumn).toBeDefined(); - expect(createdAtColumn?.type).toBe('TIMESTAMP'); + expect(createdAtColumn?.type).toBe('timestamp'); }); it('should handle ALTER TABLE ADD COLUMN with constraints', async () => { @@ -156,7 +156,7 @@ describe('PostgreSQL ALTER TABLE ADD COLUMN Tests', () => { (col) => col.name === 'name' ); expect(nameColumns).toHaveLength(1); - expect(nameColumns[0].type).toBe('VARCHAR(100)'); // Should keep original type + expect(nameColumns[0].type).toBe('varchar(100)'); // Should keep original type }); it('should use default schema when not specified', async () => { @@ -204,12 +204,12 @@ describe('PostgreSQL ALTER TABLE ADD COLUMN Tests', () => { (col) => col.name === 'my-column' ); expect(myColumn).toBeDefined(); - expect(myColumn?.type).toBe('VARCHAR(50)'); + expect(myColumn?.type).toBe('varchar(50)'); const anotherColumn = myTable.columns.find( (col) => col.name === 'another-column' ); expect(anotherColumn).toBeDefined(); - expect(anotherColumn?.type).toBe('INTEGER'); + expect(anotherColumn?.type).toBe('integer'); }); }); diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-alter-column-type.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-alter-column-type.test.ts index 2296b4cb..e097f5fb 100644 --- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-alter-column-type.test.ts +++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-alter-column-type.test.ts @@ -30,15 +30,15 @@ ALTER TABLE table_12 ALTER COLUMN field3 TYPE VARCHAR(254); // Check that the columns have the updated type const field1 = table.columns.find((col) => col.name === 'field1'); expect(field1).toBeDefined(); - expect(field1?.type).toBe('VARCHAR(254)'); // Should be updated from 200 to 254 + expect(field1?.type).toBe('varchar(254)'); // Should be updated from 200 to 254 const field2 = table.columns.find((col) => col.name === 'field2'); expect(field2).toBeDefined(); - expect(field2?.type).toBe('VARCHAR(254)'); + expect(field2?.type).toBe('varchar(254)'); const field3 = table.columns.find((col) => col.name === 'field3'); expect(field3).toBeDefined(); - expect(field3?.type).toBe('VARCHAR(254)'); + expect(field3?.type).toBe('varchar(254)'); }); it('should handle various ALTER COLUMN TYPE scenarios', async () => { @@ -65,13 +65,13 @@ ALTER TABLE test_table ALTER COLUMN score TYPE NUMERIC(10,4); const table = result.tables[0]; const nameCol = table.columns.find((col) => col.name === 'name'); - expect(nameCol?.type).toBe('VARCHAR(100)'); + expect(nameCol?.type).toBe('varchar(100)'); const ageCol = table.columns.find((col) => col.name === 'age'); - expect(ageCol?.type).toBe('INTEGER'); + expect(ageCol?.type).toBe('integer'); const scoreCol = table.columns.find((col) => col.name === 'score'); - expect(scoreCol?.type).toBe('NUMERIC(10,4)'); + expect(scoreCol?.type).toBe('numeric(10,4)'); }); it('should handle multiple type changes on the same column', async () => { @@ -101,18 +101,18 @@ ALTER TABLE table_12 ALTER COLUMN field1 TYPE BIGINT; expect(table.schema).toBe('public'); expect(table.columns).toHaveLength(4); - // Check that field1 has the final type (BIGINT), not the intermediate VARCHAR(254) + // Check that field1 has the final type (bigint), not the intermediate varchar(254) const field1 = table.columns.find((col) => col.name === 'field1'); expect(field1).toBeDefined(); - expect(field1?.type).toBe('BIGINT'); // Should be BIGINT, not VARCHAR(254) + expect(field1?.type).toBe('bigint'); // Should be bigint, not varchar(254) - // Check that field2 and field3 still have VARCHAR(254) + // Check that field2 and field3 still have varchar(254) const field2 = table.columns.find((col) => col.name === 'field2'); expect(field2).toBeDefined(); - expect(field2?.type).toBe('VARCHAR(254)'); + expect(field2?.type).toBe('varchar(254)'); const field3 = table.columns.find((col) => col.name === 'field3'); expect(field3).toBeDefined(); - expect(field3?.type).toBe('VARCHAR(254)'); + expect(field3?.type).toBe('varchar(254)'); }); }); diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-parser.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-parser.test.ts index 91034b79..eddddc90 100644 --- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-parser.test.ts +++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/postgresql-parser.test.ts @@ -19,7 +19,7 @@ describe('PostgreSQL Parser', () => { expect(result.tables[0].name).toBe('wizards'); expect(result.tables[0].columns).toHaveLength(4); expect(result.tables[0].columns[0].name).toBe('id'); - expect(result.tables[0].columns[0].type).toBe('INTEGER'); + expect(result.tables[0].columns[0].type).toBe('integer'); expect(result.tables[0].columns[0].primaryKey).toBe(true); }); @@ -81,9 +81,9 @@ describe('PostgreSQL Parser', () => { expect(result.tables).toHaveLength(1); const columns = result.tables[0].columns; - expect(columns.find((c) => c.name === 'id')?.type).toBe('UUID'); - expect(columns.find((c) => c.name === 'data')?.type).toBe('JSONB'); - expect(columns.find((c) => c.name === 'tags')?.type).toBe('TEXT[]'); + expect(columns.find((c) => c.name === 'id')?.type).toBe('uuid'); + expect(columns.find((c) => c.name === 'data')?.type).toBe('jsonb'); + expect(columns.find((c) => c.name === 'tags')?.type).toBe('text[]'); }); it('should handle numeric with precision', async () => { @@ -102,7 +102,7 @@ describe('PostgreSQL Parser', () => { const columns = result.tables[0].columns; // Parser limitation: scale on separate line is not captured const amountType = columns.find((c) => c.name === 'amount')?.type; - expect(amountType).toMatch(/^NUMERIC/); + expect(amountType).toMatch(/^numeric/); }); it('should handle multi-line numeric definitions', async () => { diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-activities-table-import.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-activities-table-import.test.ts index 000a7352..f1623714 100644 --- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-activities-table-import.test.ts +++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-activities-table-import.test.ts @@ -27,55 +27,55 @@ CREATE TABLE public.activities ( // Check each column const columns = table.columns; - // id column - serial4 should become INTEGER with auto-increment + // id column - serial4 is preserved as serial with auto-increment const idCol = columns.find((c) => c.name === 'id'); expect(idCol).toBeDefined(); - expect(idCol?.type).toBe('INTEGER'); + expect(idCol?.type).toBe('serial'); expect(idCol?.primaryKey).toBe(true); expect(idCol?.increment).toBe(true); expect(idCol?.nullable).toBe(false); - // user_id column - int4 should become INTEGER + // user_id column - int4 becomes integer const userIdCol = columns.find((c) => c.name === 'user_id'); expect(userIdCol).toBeDefined(); - expect(userIdCol?.type).toBe('INTEGER'); + expect(userIdCol?.type).toBe('integer'); expect(userIdCol?.nullable).toBe(false); // workflow_id column - int4 NULL const workflowIdCol = columns.find((c) => c.name === 'workflow_id'); expect(workflowIdCol).toBeDefined(); - expect(workflowIdCol?.type).toBe('INTEGER'); + expect(workflowIdCol?.type).toBe('integer'); expect(workflowIdCol?.nullable).toBe(true); // task_id column - int4 NULL const taskIdCol = columns.find((c) => c.name === 'task_id'); expect(taskIdCol).toBeDefined(); - expect(taskIdCol?.type).toBe('INTEGER'); + expect(taskIdCol?.type).toBe('integer'); expect(taskIdCol?.nullable).toBe(true); - // action column - character varying(50) + // action column - character varying(50) becomes varchar(50) const actionCol = columns.find((c) => c.name === 'action'); expect(actionCol).toBeDefined(); - expect(actionCol?.type).toBe('VARCHAR(50)'); + expect(actionCol?.type).toBe('varchar(50)'); expect(actionCol?.nullable).toBe(false); // description column - text const descriptionCol = columns.find((c) => c.name === 'description'); expect(descriptionCol).toBeDefined(); - expect(descriptionCol?.type).toBe('TEXT'); + expect(descriptionCol?.type).toBe('text'); expect(descriptionCol?.nullable).toBe(false); // created_at column - timestamp with default const createdAtCol = columns.find((c) => c.name === 'created_at'); expect(createdAtCol).toBeDefined(); - expect(createdAtCol?.type).toBe('TIMESTAMP'); + expect(createdAtCol?.type).toBe('timestamp'); expect(createdAtCol?.nullable).toBe(false); expect(createdAtCol?.default).toContain('NOW'); - // is_read column - bool with default + // is_read column - bool becomes boolean with default const isReadCol = columns.find((c) => c.name === 'is_read'); expect(isReadCol).toBeDefined(); - expect(isReadCol?.type).toBe('BOOLEAN'); + expect(isReadCol?.type).toBe('boolean'); expect(isReadCol?.nullable).toBe(false); expect(isReadCol?.default).toBe('FALSE'); }); @@ -106,44 +106,46 @@ CREATE TABLE type_test ( const table = result.tables[0]; const cols = table.columns; - // Check serial types - expect(cols.find((c) => c.name === 'id')?.type).toBe('INTEGER'); + // Check serial types - preserved as serial, smallserial, bigserial + expect(cols.find((c) => c.name === 'id')?.type).toBe('serial'); expect(cols.find((c) => c.name === 'id')?.increment).toBe(true); - expect(cols.find((c) => c.name === 'small_id')?.type).toBe('SMALLINT'); + expect(cols.find((c) => c.name === 'small_id')?.type).toBe( + 'smallserial' + ); expect(cols.find((c) => c.name === 'small_id')?.increment).toBe(true); - expect(cols.find((c) => c.name === 'big_id')?.type).toBe('BIGINT'); + expect(cols.find((c) => c.name === 'big_id')?.type).toBe('bigserial'); expect(cols.find((c) => c.name === 'big_id')?.increment).toBe(true); - // Check integer types - expect(cols.find((c) => c.name === 'int_col')?.type).toBe('INTEGER'); - expect(cols.find((c) => c.name === 'small_int')?.type).toBe('SMALLINT'); - expect(cols.find((c) => c.name === 'big_int')?.type).toBe('BIGINT'); + // Check integer types - normalized to lowercase + expect(cols.find((c) => c.name === 'int_col')?.type).toBe('integer'); + expect(cols.find((c) => c.name === 'small_int')?.type).toBe('smallint'); + expect(cols.find((c) => c.name === 'big_int')?.type).toBe('bigint'); - // Check boolean types - expect(cols.find((c) => c.name === 'bool_col')?.type).toBe('BOOLEAN'); + // Check boolean types - normalized to lowercase + expect(cols.find((c) => c.name === 'bool_col')?.type).toBe('boolean'); expect(cols.find((c) => c.name === 'boolean_col')?.type).toBe( - 'BOOLEAN' + 'boolean' ); - // Check string types + // Check string types - normalized to lowercase expect(cols.find((c) => c.name === 'varchar_col')?.type).toBe( - 'VARCHAR(100)' + 'varchar(100)' ); - expect(cols.find((c) => c.name === 'char_col')?.type).toBe('CHAR(10)'); - expect(cols.find((c) => c.name === 'text_col')?.type).toBe('TEXT'); + expect(cols.find((c) => c.name === 'char_col')?.type).toBe('char(10)'); + expect(cols.find((c) => c.name === 'text_col')?.type).toBe('text'); - // Check timestamp types + // Check timestamp types - normalized to lowercase expect(cols.find((c) => c.name === 'timestamp_col')?.type).toBe( - 'TIMESTAMP' + 'timestamp' ); expect(cols.find((c) => c.name === 'timestamptz_col')?.type).toBe( - 'TIMESTAMPTZ' + 'timestamptz' ); - // Check other types - expect(cols.find((c) => c.name === 'date_col')?.type).toBe('DATE'); - expect(cols.find((c) => c.name === 'time_col')?.type).toBe('TIME'); - expect(cols.find((c) => c.name === 'json_col')?.type).toBe('JSON'); - expect(cols.find((c) => c.name === 'jsonb_col')?.type).toBe('JSONB'); + // Check other types - normalized to lowercase + expect(cols.find((c) => c.name === 'date_col')?.type).toBe('date'); + expect(cols.find((c) => c.name === 'time_col')?.type).toBe('time'); + expect(cols.find((c) => c.name === 'json_col')?.type).toBe('json'); + expect(cols.find((c) => c.name === 'jsonb_col')?.type).toBe('jsonb'); }); }); diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-spell-books-junction-table.test.ts b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-spell-books-junction-table.test.ts index f597e601..f9cf5ed6 100644 --- a/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-spell-books-junction-table.test.ts +++ b/src/lib/data/sql-import/dialect-importers/postgresql/__tests__/test-spell-books-junction-table.test.ts @@ -42,14 +42,14 @@ CREATE TABLE book_spells ( (c) => c.name === 'spell_book_id' ); expect(spellBookIdColumn).toBeDefined(); - expect(spellBookIdColumn!.type).toBe('UUID'); + expect(spellBookIdColumn!.type).toBe('uuid'); expect(spellBookIdColumn!.nullable).toBe(false); const spellIdColumn = bookSpells!.columns.find( (c) => c.name === 'spell_id' ); expect(spellIdColumn).toBeDefined(); - expect(spellIdColumn!.type).toBe('UUID'); + expect(spellIdColumn!.type).toBe('uuid'); expect(spellIdColumn!.nullable).toBe(false); }); diff --git a/src/lib/data/sql-import/dialect-importers/postgresql/postgresql.ts b/src/lib/data/sql-import/dialect-importers/postgresql/postgresql.ts index 664e400d..a8c39a04 100644 --- a/src/lib/data/sql-import/dialect-importers/postgresql/postgresql.ts +++ b/src/lib/data/sql-import/dialect-importers/postgresql/postgresql.ts @@ -249,69 +249,159 @@ function splitSQLStatements(sql: string): string[] { } /** - * Normalize PostgreSQL type aliases to standard types + * Set of serial type names for O(1) lookup */ -function normalizePostgreSQLType(type: string): string { +const SERIAL_TYPES = new Set([ + 'SERIAL', + 'SERIAL2', + 'SERIAL4', + 'SERIAL8', + 'BIGSERIAL', + 'SMALLSERIAL', +]); + +/** + * Check if a type is a serial type + */ +function isSerialTypeName(typeName: string): boolean { + return SERIAL_TYPES.has(typeName.toUpperCase().split('(')[0]); +} + +/** + * Normalize PostgreSQL type syntax to lowercase canonical form. + * This function handles parsing-level normalization only - it converts + * verbose SQL syntax to the preferred short form that getPreferredSynonym + * expects. It preserves semantic types like serial (does NOT convert to integer). + * + * The optional `length` parameter is used to resolve ambiguous types where + * the SQL parser returns a base type with a length modifier (e.g., 'SERIAL' + * with length=2 for 'serial2', or 'INT' with length=8 for 'int8'). + * + * Type synonym resolution (e.g., integer→int) is handled by getPreferredSynonym. + */ +function normalizePostgreSQLType( + type: string, + length?: number | undefined +): string { const upperType = type.toUpperCase(); - // Handle types with parameters - more complex regex to handle CHARACTER VARYING + // Handle types with parameters (e.g., VARCHAR(255), NUMERIC(10,2)) const typeMatch = upperType.match(/^([\w\s]+?)(\(.+\))?$/); - if (!typeMatch) return type; + if (!typeMatch) return type.toLowerCase(); const baseType = typeMatch[1].trim(); const params = typeMatch[2] || ''; let normalizedBase: string; switch (baseType) { - // Serial types + // Serial types - preserve as-is (they are valid PostgreSQL types) + // Handle parser quirk: 'SERIAL' with length=2 means 'serial2' (smallserial) case 'SERIAL': + if (length === 2) { + normalizedBase = 'smallserial'; + } else if (length === 8) { + normalizedBase = 'bigserial'; + } else { + normalizedBase = 'serial'; + } + break; case 'SERIAL4': - normalizedBase = 'INTEGER'; + normalizedBase = 'serial'; break; case 'BIGSERIAL': case 'SERIAL8': - normalizedBase = 'BIGINT'; + normalizedBase = 'bigserial'; break; case 'SMALLSERIAL': case 'SERIAL2': - normalizedBase = 'SMALLINT'; + normalizedBase = 'smallserial'; break; - // Integer aliases + // Integer types - normalize to lowercase canonical form + // Handle parser quirk: 'INT' with length=2 means 'int2' (smallint) case 'INT': + if (length === 2) { + normalizedBase = 'smallint'; + } else if (length === 8) { + normalizedBase = 'bigint'; + } else { + normalizedBase = 'integer'; + } + break; case 'INT4': - normalizedBase = 'INTEGER'; + case 'INTEGER': + normalizedBase = 'integer'; break; case 'INT2': - normalizedBase = 'SMALLINT'; + case 'SMALLINT': + normalizedBase = 'smallint'; break; case 'INT8': - normalizedBase = 'BIGINT'; + case 'BIGINT': + normalizedBase = 'bigint'; break; - // Boolean aliases + // Boolean case 'BOOL': - normalizedBase = 'BOOLEAN'; + case 'BOOLEAN': + normalizedBase = 'boolean'; break; - // Character types - use common names + // Character types - normalize verbose forms case 'CHARACTER VARYING': + normalizedBase = 'varchar'; + break; case 'VARCHAR': - normalizedBase = 'VARCHAR'; + normalizedBase = 'varchar'; break; case 'CHARACTER': - case 'CHAR': - normalizedBase = 'CHAR'; + normalizedBase = 'char'; break; - // Timestamp aliases + case 'CHAR': + normalizedBase = 'char'; + break; + // Timestamp types case 'TIMESTAMPTZ': case 'TIMESTAMP WITH TIME ZONE': - normalizedBase = 'TIMESTAMPTZ'; + normalizedBase = 'timestamptz'; + break; + case 'TIMESTAMP WITHOUT TIME ZONE': + case 'TIMESTAMP': + normalizedBase = 'timestamp'; + break; + // Time types + case 'TIMETZ': + case 'TIME WITH TIME ZONE': + normalizedBase = 'timetz'; + break; + case 'TIME WITHOUT TIME ZONE': + case 'TIME': + normalizedBase = 'time'; + break; + // Floating point + case 'FLOAT4': + case 'REAL': + normalizedBase = 'real'; + break; + case 'FLOAT8': + case 'DOUBLE PRECISION': + normalizedBase = 'double precision'; + break; + // Bit types + case 'BIT VARYING': + normalizedBase = 'varbit'; + break; + // Numeric types + case 'DECIMAL': + normalizedBase = 'numeric'; + break; + case 'NUMERIC': + normalizedBase = 'numeric'; break; default: - // For unknown types (like enums), preserve original case - return type; + // For unknown types (like enums, user-defined), preserve original in lowercase + return type.toLowerCase(); } - // Return normalized type with original parameters preserved - return normalizedBase + params; + // Return normalized type with parameters preserved (lowercase) + return normalizedBase + params.toLowerCase(); } /** @@ -372,17 +462,9 @@ function extractColumnsFromSQL(sql: string): SQLColumn[] { } // Check if it's a serial type for increment flag - const upperType = columnType.toUpperCase(); - const isSerialType = [ - 'SERIAL', - 'SERIAL2', - 'SERIAL4', - 'SERIAL8', - 'BIGSERIAL', - 'SMALLSERIAL', - ].includes(upperType.split('(')[0]); + const isSerialType = isSerialTypeName(columnType); - // Normalize the type + // Normalize the type (preserves serial types) columnType = normalizePostgreSQLType(columnType); // Check for common constraints @@ -820,121 +902,76 @@ export async function fromPostgres( } } - // First normalize the base type - let normalizedBaseType = rawDataType; - let isSerialType = false; - - // Check if it's a serial type first - const upperType = rawDataType.toUpperCase(); - const typeLength = definition?.length as + // Check if it's a serial type + const isSerialType = isSerialTypeName(rawDataType); + const typeLength = columnDef.definition?.length as | number | undefined; - if (upperType === 'SERIAL') { - // Use length to determine the actual serial type - if (typeLength === 2) { - normalizedBaseType = 'SMALLINT'; - isSerialType = true; - } else if (typeLength === 8) { - normalizedBaseType = 'BIGINT'; - isSerialType = true; - } else { - // Default serial or serial4 - normalizedBaseType = 'INTEGER'; - isSerialType = true; - } - } else if (upperType === 'SMALLSERIAL') { - normalizedBaseType = 'SMALLINT'; - isSerialType = true; - } else if (upperType === 'BIGSERIAL') { - normalizedBaseType = 'BIGINT'; - isSerialType = true; - } else if (upperType === 'INT') { - // Use length to determine the actual int type - if (typeLength === 2) { - normalizedBaseType = 'SMALLINT'; - } else if (typeLength === 8) { - normalizedBaseType = 'BIGINT'; - } else { - // Default int or int4 - normalizedBaseType = 'INTEGER'; - } - } else { - // Apply normalization for other types - normalizedBaseType = - normalizePostgreSQLType(rawDataType); - } + // Normalize the type (pass length to handle parser quirks like INT with length=8) + let finalDataType = normalizePostgreSQLType( + rawDataType, + typeLength + ); - // Now handle parameters - but skip for integer types that shouldn't have them - let finalDataType = normalizedBaseType; - - // Don't add parameters to INTEGER types that come from int4, int8, serial types, etc. - const isNormalizedIntegerType = - ['INTEGER', 'BIGINT', 'SMALLINT'].includes( - normalizedBaseType - ) && - [ - 'INT', - 'SERIAL', - 'SMALLSERIAL', - 'BIGSERIAL', - ].includes(upperType); - - if (!isSerialType && !isNormalizedIntegerType) { - // Include precision/scale/length in the type string if available + // Add type parameters for non-serial, non-integer types + if (!isSerialType) { const precision = columnDef.definition?.precision; const scale = columnDef.definition?.scale; - const length = columnDef.definition?.length; - - // Also check if there's a suffix that includes the precision/scale - const definition = + const suffix = ( columnDef.definition as Record< string, unknown - >; - const suffix = definition?.suffix; + > + )?.suffix; - if ( - suffix && - Array.isArray(suffix) && - suffix.length > 0 - ) { - // The suffix contains the full type parameters like (10,2) - const params = suffix - .map((s: unknown) => { - if ( + // Skip adding parameters to integer types (they don't have size params) + const isIntegerType = [ + 'integer', + 'bigint', + 'smallint', + ].includes(finalDataType); + + if (!isIntegerType) { + if ( + suffix && + Array.isArray(suffix) && + suffix.length > 0 + ) { + const params = suffix + .map((s: unknown) => typeof s === 'object' && s !== null && 'value' in s - ) { - return String( - (s as { value: unknown }) - .value - ); - } - return String(s); - }) - .join(','); - finalDataType = `${normalizedBaseType}(${params})`; - } else if (precision !== undefined) { - if (scale !== undefined) { - finalDataType = `${normalizedBaseType}(${precision},${scale})`; - } else { - finalDataType = `${normalizedBaseType}(${precision})`; + ? String( + ( + s as { + value: unknown; + } + ).value + ) + : String(s) + ) + .join(','); + finalDataType = `${finalDataType}(${params})`; + } else if (precision !== undefined) { + finalDataType = + scale !== undefined + ? `${finalDataType}(${precision},${scale})` + : `${finalDataType}(${precision})`; + } else if ( + scale !== undefined && + typeLength !== undefined + ) { + // For NUMERIC, node-sql-parser stores precision as 'length' + finalDataType = `${finalDataType}(${typeLength},${scale})`; + } else if ( + typeLength !== undefined && + typeLength !== null + ) { + finalDataType = `${finalDataType}(${typeLength})`; } - } else if ( - scale !== undefined && - length !== undefined - ) { - // For DECIMAL/NUMERIC, node-sql-parser stores precision as 'length' - finalDataType = `${normalizedBaseType}(${length},${scale})`; - } else if ( - length !== undefined && - length !== null - ) { - // For VARCHAR, CHAR, etc. - finalDataType = `${normalizedBaseType}(${length})`; } } @@ -1259,81 +1296,61 @@ export async function fromPostgres( const rawDataType = String( definition?.dataType || 'TEXT' ); - // console.log('expr:', JSON.stringify(expr, null, 2)); - - // Normalize the type - let normalizedBaseType = - normalizePostgreSQLType(rawDataType); // Check if it's a serial type - const upperType = rawDataType.toUpperCase(); - const isSerialType = [ - 'SERIAL', - 'SERIAL2', - 'SERIAL4', - 'SERIAL8', - 'BIGSERIAL', - 'SMALLSERIAL', - ].includes(upperType.split('(')[0]); + const isSerialType = isSerialTypeName(rawDataType); + const typeLength = definition?.length as + | number + | undefined; - if (isSerialType) { - const typeLength = definition?.length as - | number - | undefined; - if (upperType === 'SERIAL') { - if (typeLength === 2) { - normalizedBaseType = 'SMALLINT'; - } else if (typeLength === 8) { - normalizedBaseType = 'BIGINT'; - } else { - normalizedBaseType = 'INTEGER'; - } - } - } + // Normalize the type (pass length to handle parser quirks) + let finalDataType = normalizePostgreSQLType( + rawDataType, + typeLength + ); - // Handle type parameters - let finalDataType = normalizedBaseType; - const isNormalizedIntegerType = - ['INTEGER', 'BIGINT', 'SMALLINT'].includes( - normalizedBaseType - ) && - (upperType === 'INT' || upperType === 'SERIAL'); - - if (!isSerialType && !isNormalizedIntegerType) { + // Add type parameters for non-serial, non-integer types + if (!isSerialType) { const precision = definition?.precision; const scale = definition?.scale; - const length = definition?.length; const suffix = (definition?.suffix as unknown[]) || []; - if (suffix.length > 0) { - const params = suffix - .map((s: unknown) => { - if ( + const isIntegerType = [ + 'integer', + 'bigint', + 'smallint', + ].includes(finalDataType); + + if (!isIntegerType) { + if (suffix.length > 0) { + const params = suffix + .map((s: unknown) => typeof s === 'object' && s !== null && 'value' in s - ) { - return String( - (s as { value: unknown }) - .value - ); - } - return String(s); - }) - .join(','); - finalDataType = `${normalizedBaseType}(${params})`; - } else if (precision !== undefined) { - if (scale !== undefined) { - finalDataType = `${normalizedBaseType}(${precision},${scale})`; - } else { - finalDataType = `${normalizedBaseType}(${precision})`; + ? String( + ( + s as { + value: unknown; + } + ).value + ) + : String(s) + ) + .join(','); + finalDataType = `${finalDataType}(${params})`; + } else if (precision !== undefined) { + finalDataType = + scale !== undefined + ? `${finalDataType}(${precision},${scale})` + : `${finalDataType}(${precision})`; + } else if ( + typeLength !== undefined && + typeLength !== null + ) { + finalDataType = `${finalDataType}(${typeLength})`; } - } else if ( - length !== undefined && - length !== null - ) { - finalDataType = `${normalizedBaseType}(${length})`; } } @@ -1429,84 +1446,62 @@ export async function fromPostgres( definition?.dataType || 'TEXT' ); - // Normalize the type - let normalizedBaseType = - normalizePostgreSQLType(rawDataType); - // Check if it's a serial type - const upperType = rawDataType.toUpperCase(); - const isSerialType = [ - 'SERIAL', - 'SERIAL2', - 'SERIAL4', - 'SERIAL8', - 'BIGSERIAL', - 'SMALLSERIAL', - ].includes(upperType.split('(')[0]); + const isSerialType = + isSerialTypeName(rawDataType); + const typeLength = definition?.length as + | number + | undefined; - if (isSerialType) { - const typeLength = definition?.length as - | number - | undefined; - if (upperType === 'SERIAL') { - if (typeLength === 2) { - normalizedBaseType = 'SMALLINT'; - } else if (typeLength === 8) { - normalizedBaseType = 'BIGINT'; - } else { - normalizedBaseType = 'INTEGER'; - } - } - } + // Normalize the type (pass length to handle parser quirks) + let finalDataType = normalizePostgreSQLType( + rawDataType, + typeLength + ); - // Handle type parameters - let finalDataType = normalizedBaseType; - const isNormalizedIntegerType = - ['INTEGER', 'BIGINT', 'SMALLINT'].includes( - normalizedBaseType - ) && - (upperType === 'INT' || - upperType === 'SERIAL'); - - if (!isSerialType && !isNormalizedIntegerType) { + // Add type parameters for non-serial, non-integer types + if (!isSerialType) { const precision = columnDef.definition?.precision; const scale = columnDef.definition?.scale; - const length = columnDef.definition?.length; const suffix = (definition?.suffix as unknown[]) || []; - if (suffix.length > 0) { - const params = suffix - .map((s: unknown) => { - if ( + const isIntegerType = [ + 'integer', + 'bigint', + 'smallint', + ].includes(finalDataType); + + if (!isIntegerType) { + if (suffix.length > 0) { + const params = suffix + .map((s: unknown) => typeof s === 'object' && s !== null && 'value' in s - ) { - return String( - ( - s as { - value: unknown; - } - ).value - ); - } - return String(s); - }) - .join(','); - finalDataType = `${normalizedBaseType}(${params})`; - } else if (precision !== undefined) { - if (scale !== undefined) { - finalDataType = `${normalizedBaseType}(${precision},${scale})`; - } else { - finalDataType = `${normalizedBaseType}(${precision})`; + ? String( + ( + s as { + value: unknown; + } + ).value + ) + : String(s) + ) + .join(','); + finalDataType = `${finalDataType}(${params})`; + } else if (precision !== undefined) { + finalDataType = + scale !== undefined + ? `${finalDataType}(${precision},${scale})` + : `${finalDataType}(${precision})`; + } else if ( + typeLength !== undefined && + typeLength !== null + ) { + finalDataType = `${finalDataType}(${typeLength})`; } - } else if ( - length !== undefined && - length !== null - ) { - finalDataType = `${normalizedBaseType}(${length})`; } }