diff --git a/drizzle-kit/src/introspect-pg.ts b/drizzle-kit/src/introspect-pg.ts index 4bb65ee0c..4058713b5 100644 --- a/drizzle-kit/src/introspect-pg.ts +++ b/drizzle-kit/src/introspect-pg.ts @@ -275,7 +275,9 @@ export const relationsToTypeScriptForStudio = ( }; function generateIdentityParams(identity: Column['identity']) { - let paramsObj = `{ name: "${identity!.name}"`; + let paramsObj = `{ name: ${ + identity!.name.startsWith('"') && identity!.name.endsWith('"') ? identity!.name : `"${identity!.name}"` + }`; if (identity?.startWith) { paramsObj += `, startWith: ${identity.startWith}`; } @@ -818,8 +820,9 @@ const mapDefault = ( return typeof defaultValue !== 'undefined' ? `.default(${mapColumnDefault(defaultValue, isExpression)})` : ''; } - if (lowered.startsWith('geometry')) { - return typeof defaultValue !== 'undefined' ? `.default(${mapColumnDefault(defaultValue, isExpression)})` : ''; + if (lowered === 'geometry(point)') { + const match = defaultValue?.match(/\d+/g) ?? []; + return typeof defaultValue !== 'undefined' ? `.default([${match[0]}, ${match[1]}])` : ''; } if (lowered.startsWith('vector')) { diff --git a/drizzle-kit/src/jsonStatements.ts b/drizzle-kit/src/jsonStatements.ts index b70d01b99..803a3ddf9 100644 --- a/drizzle-kit/src/jsonStatements.ts +++ b/drizzle-kit/src/jsonStatements.ts @@ -470,6 +470,17 @@ export interface JsonAlterColumnTypeStatement { columnAutoIncrement: boolean; columnPk: boolean; columnGenerated?: { as: string; type: 'stored' | 'virtual' }; + identity?: { + type: 'added'; + value: string; + } | { + type: 'deleted'; + value: string; + } | { + type: 'changed'; + old: string; + new: string; + } | undefined; } export interface JsonAlterColumnSetPrimaryKeyStatement { @@ -558,6 +569,7 @@ export interface JsonAlterColumnSetIdentityStatement { columnName: string; schema: string; identity: string; + changedSerialToIntegerIdentity: boolean; } export interface JsonAlterColumnDropIdentityStatement { @@ -2100,6 +2112,7 @@ export const preparePgAlterColumns = ( columnNotNull, columnAutoIncrement, columnPk, + identity: column.identity, }); } @@ -2219,6 +2232,8 @@ export const preparePgAlterColumns = ( columnName, schema, identity: column.identity.value, + changedSerialToIntegerIdentity: column.type?.type === 'changed' && column.type.old === 'serial' + && column.type.new === 'integer' && column.identity.type === 'added', }); } diff --git a/drizzle-kit/src/serializer/mysqlSerializer.ts b/drizzle-kit/src/serializer/mysqlSerializer.ts index aaa1acb82..8d9de8c62 100644 --- a/drizzle-kit/src/serializer/mysqlSerializer.ts +++ b/drizzle-kit/src/serializer/mysqlSerializer.ts @@ -26,7 +26,7 @@ import { UniqueConstraint, View, } from '../serializer/mysqlSchema'; -import { type DB, escapeSingleQuotes } from '../utils'; +import { type DB, escapeSingleQuotes, replaceQueryParams } from '../utils'; import { getColumnCasing, sqlToStr } from './utils'; export const indexName = (tableName: string, columns: string[]) => { @@ -167,6 +167,8 @@ export const generateMySqlSnapshot = ( .slice(0, 23) }'`; } + } else if (typeof column.default === 'bigint') { + columnToSet.default = column.default.toString(); } else { columnToSet.default = column.default; } @@ -399,7 +401,7 @@ export const generateMySqlSnapshot = ( checkConstraintObject[checkName] = { name: checkName, - value: dialect.sqlToQuery(check.value).sql, + value: replaceQueryParams('mysql', dialect.sqlToQuery(check.value)), }; }); @@ -486,6 +488,8 @@ export const generateMySqlSnapshot = ( } else { if (typeof column.default === 'string') { columnToSet.default = `'${column.default}'`; + } else if (typeof column.default === 'bigint') { + columnToSet.default = column.default.toString(); } else { if (sqlTypeLowered === 'json') { columnToSet.default = `'${JSON.stringify(column.default)}'`; @@ -973,7 +977,7 @@ AND const tableName = checkConstraintRow['TABLE_NAME']; const tableInResult = result[tableName]; - // if (typeof tableInResult === 'undefined') continue; + if (typeof tableInResult === 'undefined') continue; tableInResult.checkConstraint[constraintName] = { name: constraintName, diff --git a/drizzle-kit/src/serializer/pgSerializer.ts b/drizzle-kit/src/serializer/pgSerializer.ts index b0faa5ea8..d0d8e7bdb 100644 --- a/drizzle-kit/src/serializer/pgSerializer.ts +++ b/drizzle-kit/src/serializer/pgSerializer.ts @@ -39,7 +39,7 @@ import type { UniqueConstraint, View, } from '../serializer/pgSchema'; -import { type DB, escapeSingleQuotes, isPgArrayType } from '../utils'; +import { type DB, escapeSingleQuotes, isPgArrayType, replaceQueryParams } from '../utils'; import { getColumnCasing, sqlToStr } from './utils'; export const indexName = (tableName: string, columns: string[]) => { @@ -239,6 +239,11 @@ export const generatePgSnapshot = ( if (column.default !== undefined) { if (is(column.default, SQL)) { columnToSet.default = sqlToStr(column.default, casing); + } else if (sqlTypeLowered === 'geometry(point)') { + const def = Array.isArray(column.default) + ? column.default + : [(column.default as any).x, (column.default as any).y]; + columnToSet.default = `st_geomfromtext('point(${def[0]}, ${def[1]})'::text)`; } else { if (typeof column.default === 'string') { columnToSet.default = `'${escapeSingleQuotes(column.default)}'`; @@ -255,6 +260,8 @@ export const generatePgSnapshot = ( } } else if (isPgArrayType(sqlTypeLowered) && Array.isArray(column.default)) { columnToSet.default = `'${buildArrayString(column.default, sqlTypeLowered)}'`; + } else if (typeof column.default === 'bigint') { + columnToSet.default = column.default.toString(); } else { // Should do for all types // columnToSet.default = `'${column.default}'::${sqlTypeLowered}`; @@ -559,7 +566,7 @@ export const generatePgSnapshot = ( checksObject[checkName] = { name: checkName, - value: dialect.sqlToQuery(check.value).sql, + value: replaceQueryParams('postgresql', dialect.sqlToQuery(check.value)), }; }); @@ -819,11 +826,18 @@ export const generatePgSnapshot = ( if (column.default !== undefined) { if (is(column.default, SQL)) { columnToSet.default = sqlToStr(column.default, casing); + } else if (sqlTypeLowered === 'geometry(point)') { + const def = Array.isArray(column.default) + ? column.default + : [(column.default as any).x, (column.default as any).y]; + columnToSet.default = `st_geomfromtext('point(${def[0]}, ${def[1]})'::text)`; } else { if (typeof column.default === 'string') { columnToSet.default = `'${column.default}'`; + } else if (typeof column.default === 'bigint') { + columnToSet.default = column.default.toString(); } else { - if (sqlTypeLowered === 'jsonb' || sqlTypeLowered === 'json') { + if (sqlTypeLowered === 'jsonb' || sqlTypeLowered === 'json' || sqlTypeLowered.startsWith('geometry')) { columnToSet.default = `'${JSON.stringify(column.default)}'::${sqlTypeLowered}`; } else if (column.default instanceof Date) { if (sqlTypeLowered === 'date') { @@ -1220,12 +1234,16 @@ WHERE const tableResponse = await getColumnsInfoQuery({ schema: tableSchema, table: tableName, db }); const tableConstraints = await db.query( - `SELECT c.column_name, c.data_type, constraint_type, constraint_name, constraint_schema + `WITH constraints AS ( + SELECT c.column_name, c.data_type, constraint_type, constraint_name, constraint_schema FROM information_schema.table_constraints tc JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_name) JOIN information_schema.columns AS c ON c.table_schema = tc.constraint_schema AND tc.table_name = c.table_name AND ccu.column_name = c.column_name - WHERE tc.table_name = '${tableName}' and constraint_schema = '${tableSchema}';`, + WHERE tc.table_name = '${tableName}' and constraint_schema = '${tableSchema}' + ) SELECT DISTINCT ON (c.column_name, c.constraint_name, c.constraint_schema) c.*, kcu.ordinal_position as position FROM constraints as c + LEFT JOIN information_schema.key_column_usage AS kcu USING (constraint_schema, constraint_name, column_name) + ORDER BY c.column_name, c.constraint_name, c.constraint_schema, position;`, ); const tableChecks = await db.query(`SELECT @@ -1387,7 +1405,10 @@ WHERE const identityMaximum = columnResponse.identity_maximum; const identityMinimum = columnResponse.identity_minimum; const identityCycle = columnResponse.identity_cycle === 'YES'; - const identityName = columnResponse.seq_name; + const identityName = columnResponse.seq_name && columnResponse.seq_name.startsWith('"') + && columnResponse.seq_name.endsWith('"') + ? columnResponse.seq_name.slice(1, -1) + : columnResponse.seq_name; const primaryKey = tableConstraints.filter((mapRow) => columnName === mapRow.column_name && mapRow.constraint_type === 'PRIMARY KEY' @@ -1478,6 +1499,9 @@ WHERE .replace('character', 'char'); columnTypeMapped = trimChar(columnTypeMapped, '"'); + columnTypeMapped = columnTypeMapped === 'geometry(Point)' + ? columnTypeMapped.toLowerCase() + : columnTypeMapped; columnToReturn[columnName] = { name: columnName, @@ -1780,6 +1804,9 @@ WHERE .replace('character', 'char'); columnTypeMapped = trimChar(columnTypeMapped, '"'); + columnTypeMapped = columnTypeMapped === 'geometry(Point)' + ? columnTypeMapped.toLowerCase() + : columnTypeMapped; columnToReturn[columnName] = { name: columnName, diff --git a/drizzle-kit/src/serializer/singlestoreSerializer.ts b/drizzle-kit/src/serializer/singlestoreSerializer.ts index e65f53d25..12fb35574 100644 --- a/drizzle-kit/src/serializer/singlestoreSerializer.ts +++ b/drizzle-kit/src/serializer/singlestoreSerializer.ts @@ -129,6 +129,8 @@ export const generateSingleStoreSnapshot = ( } else { if (typeof column.default === 'string') { columnToSet.default = `'${column.default}'`; + } else if (typeof column.default === 'bigint') { + columnToSet.default = column.default.toString(); } else { if (sqlTypeLowered === 'json' || Array.isArray(column.default)) { columnToSet.default = `'${JSON.stringify(column.default)}'`; @@ -359,6 +361,8 @@ export const generateSingleStoreSnapshot = ( } else { if (typeof column.default === 'string') { columnToSet.default = `'${column.default}'`; + } else if (typeof column.default === 'bigint') { + columnToSet.default = column.default.toString(); } else { if (sqlTypeLowered === 'json') { columnToSet.default = `'${JSON.stringify(column.default)}'`; diff --git a/drizzle-kit/src/serializer/sqliteSerializer.ts b/drizzle-kit/src/serializer/sqliteSerializer.ts index 107a1b292..afdd8554f 100644 --- a/drizzle-kit/src/serializer/sqliteSerializer.ts +++ b/drizzle-kit/src/serializer/sqliteSerializer.ts @@ -25,7 +25,7 @@ import type { UniqueConstraint, View, } from '../serializer/sqliteSchema'; -import { escapeSingleQuotes, type SQLiteDB } from '../utils'; +import { escapeSingleQuotes, replaceQueryParams, type SQLiteDB } from '../utils'; import { getColumnCasing, sqlToStr } from './utils'; export const generateSqliteSnapshot = ( @@ -91,6 +91,8 @@ export const generateSqliteSnapshot = ( } else { columnToSet.default = typeof column.default === 'string' ? `'${escapeSingleQuotes(column.default)}'` + : typeof column.default === 'bigint' + ? column.default.toString() : typeof column.default === 'object' || Array.isArray(column.default) ? `'${JSON.stringify(column.default)}'` @@ -309,7 +311,7 @@ export const generateSqliteSnapshot = ( checkConstraintObject[checkName] = { name: checkName, - value: dialect.sqlToQuery(check.value).sql, + value: replaceQueryParams('sqlite', dialect.sqlToQuery(check.value)), }; }); @@ -378,6 +380,8 @@ export const generateSqliteSnapshot = ( } else { columnToSet.default = typeof column.default === 'string' ? `'${column.default}'` + : typeof column.default === 'bigint' + ? column.default.toString() : typeof column.default === 'object' || Array.isArray(column.default) ? `'${JSON.stringify(column.default)}'` diff --git a/drizzle-kit/src/sqlgenerator.ts b/drizzle-kit/src/sqlgenerator.ts index 4843c6c0c..86fee180f 100644 --- a/drizzle-kit/src/sqlgenerator.ts +++ b/drizzle-kit/src/sqlgenerator.ts @@ -93,6 +93,7 @@ import { escapeSingleQuotes } from './utils'; const parseType = (schemaPrefix: string, type: string) => { const pgNativeTypes = [ + 'bit', 'uuid', 'smallint', 'integer', @@ -1006,7 +1007,7 @@ class PgAlterTableAlterColumnSetGenerated extends Convertor { override convert( statement: JsonAlterColumnSetIdentityStatement, ): string | string[] { - const { identity, tableName, columnName, schema } = statement; + const { identity, tableName, columnName, schema, changedSerialToIntegerIdentity } = statement; const tableNameWithSchema = schema ? `"${schema}"."${tableName}"` @@ -1042,6 +1043,17 @@ class PgAlterTableAlterColumnSetGenerated extends Convertor { })` : ''; + const statementSql = `ALTER TABLE ${tableNameWithSchema} ALTER COLUMN "${columnName}" ADD${identityStatement};`; + + if (changedSerialToIntegerIdentity) { + const getSerialStatement = `pg_get_serial_sequence('${schema ? `${schema}.` : ''}${tableName}', '${columnName}')`; + const coalesceStatement = `coalesce(max("${columnName}"), 0) + 1`; + return [ + statementSql, + `SELECT setval(${getSerialStatement}, ${coalesceStatement}, false) FROM ${tableNameWithSchema};`, + ]; + } + return `ALTER TABLE ${tableNameWithSchema} ALTER COLUMN "${columnName}" ADD${identityStatement};`; } } @@ -1877,12 +1889,26 @@ class PgAlterTableAlterColumnSetTypeConvertor extends Convertor { } convert(statement: JsonAlterColumnTypeStatement) { - const { tableName, columnName, newDataType, schema } = statement; + const { tableName, columnName, newDataType, schema, oldDataType, identity } = statement; const tableNameWithSchema = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`; + // Switching from serial data type to using integer generated always as identity + if (oldDataType === 'serial' && newDataType === 'integer' && identity?.type === 'added') { + const unsquashedIdentity = PgSquasher.unsquashIdentity(identity.value); + + const identityWithSchema = schema + ? `"${schema}"."${unsquashedIdentity?.name}"` + : `"${unsquashedIdentity?.name}"`; + + return [ + `ALTER TABLE ${tableNameWithSchema} ALTER COLUMN "${columnName}" DROP DEFAULT;`, + `DROP SEQUENCE ${identityWithSchema};`, + ]; + } + return `ALTER TABLE ${tableNameWithSchema} ALTER COLUMN "${columnName}" SET DATA TYPE ${newDataType};`; } } diff --git a/drizzle-kit/src/utils.ts b/drizzle-kit/src/utils.ts index 93eb044e0..aff975f0b 100644 --- a/drizzle-kit/src/utils.ts +++ b/drizzle-kit/src/utils.ts @@ -1,5 +1,6 @@ import type { RunResult } from 'better-sqlite3'; import chalk from 'chalk'; +import type { Query } from 'drizzle-orm'; import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import { parse } from 'url'; @@ -107,6 +108,7 @@ export const prepareOutFolder = (out: string, dialect: Dialect) => { const snapshots = readdirSync(meta) .filter((it) => !it.startsWith('_')) + .filter((it) => it.endsWith('.json')) .map((it) => join(meta, it)); snapshots.sort(); @@ -365,6 +367,30 @@ export function escapeSingleQuotes(str: string) { } export function unescapeSingleQuotes(str: string, ignoreFirstAndLastChar: boolean) { + if (str === "''") return str; const regex = ignoreFirstAndLastChar ? /(? { + try { + return JSON.stringify(p); + } catch { + return String(p); + } + }); + + if (dialect === 'postgresql') { + for (let i = 0; i < params.length; i++) { + str = str.replace(`$${i + 1}`, params[i]); + } + return str; + } + + for (const param of params) { + str = str.replace('?', param); + } + return str; +} diff --git a/drizzle-kit/tests/introspect/mysql.test.ts b/drizzle-kit/tests/introspect/mysql.test.ts index 2db33416b..07980dd2a 100644 --- a/drizzle-kit/tests/introspect/mysql.test.ts +++ b/drizzle-kit/tests/introspect/mysql.test.ts @@ -317,3 +317,24 @@ test('instrospect strings with single quotes', async () => { await client.query(`drop table columns;`); }); + +test('introspect strings with empty string as default', async () => { + const schema = { + columns: mysqlTable('columns', { + text: text('text').default(''), + varchar: varchar('varchar', { length: 255 }).default(''), + }), + }; + + const { statements, sqlStatements } = await introspectMySQLToFile( + client, + schema, + 'introspect-strings-with-empty-string-as-default', + 'drizzle', + ); + + expect(statements.length).toBe(0); + expect(sqlStatements.length).toBe(0); + + await client.query(`drop table columns;`); +}); diff --git a/drizzle-kit/tests/introspect/pg.test.ts b/drizzle-kit/tests/introspect/pg.test.ts index 1d9f0f18c..9de7ae02a 100644 --- a/drizzle-kit/tests/introspect/pg.test.ts +++ b/drizzle-kit/tests/introspect/pg.test.ts @@ -422,7 +422,7 @@ test('introspect enum with similar name to native type', async () => { expect(sqlStatements.length).toBe(0); }); -test('instrospect strings with single quotes', async () => { +test('introspect strings with single quotes', async () => { const client = new PGlite(); const myEnum = pgEnum('my_enum', ['escape\'s quotes " ']); @@ -445,6 +445,26 @@ test('instrospect strings with single quotes', async () => { expect(sqlStatements.length).toBe(0); }); +test('introspect strings with empty string as default', async () => { + const client = new PGlite(); + + const schema = { + columns: pgTable('columns', { + text: text('text').default(''), + varchar: varchar('varchar').default(''), + }), + }; + + const { statements, sqlStatements } = await introspectPgToFile( + client, + schema, + 'introspect-strings-with-empty-string-as-default', + ); + + expect(statements.length).toBe(0); + expect(sqlStatements.length).toBe(0); +}); + test('introspect checks', async () => { const client = new PGlite(); @@ -892,3 +912,30 @@ test('multiple policies with roles from schema', async () => { expect(statements.length).toBe(0); expect(sqlStatements.length).toBe(0); }); + +test('view with policy', async () => { + const client = new PGlite(); + + const schema = { + users: pgTable('users', { + id: integer('id').primaryKey(), + }), + view: pgView('view', { + id: integer('id').primaryKey(), + }).with({ + checkOption: 'cascaded', + securityBarrier: false, + securityInvoker: true, + }).as(sql`select * from "users"`), + }; + + const { statements, sqlStatements } = await introspectPgToFile( + client, + schema, + 'view-with-policy', + ['public'], + ); + + expect(statements.length).toBe(0); + expect(sqlStatements.length).toBe(0); +}); diff --git a/drizzle-kit/tests/introspect/postgis.test.ts b/drizzle-kit/tests/introspect/postgis.test.ts new file mode 100644 index 000000000..6e84479f7 --- /dev/null +++ b/drizzle-kit/tests/introspect/postgis.test.ts @@ -0,0 +1,125 @@ +import 'dotenv/config'; +import Docker from 'dockerode'; +import { geometry, integer, pgTable } from 'drizzle-orm/pg-core'; +import fs from 'fs'; +import getPort from 'get-port'; +import { Client } from 'pg'; +import { introspectPgToFile } from 'tests/schemaDiffer'; +import { v4 as uuid } from 'uuid'; +import { afterAll, beforeAll, expect, test } from 'vitest'; + +if (!fs.existsSync('tests/introspect/postgres')) { + fs.mkdirSync('tests/introspect/postgres'); +} + +let client: Client; +let pgContainer: Docker.Container; +const ignoreTables = ['public.spatial_ref_sys', 'public.geography_columns', 'public.geometry_columns']; + +export async function createDockerDB(): Promise<{ connectionString: string; container: Docker.Container }> { + const docker = new Docker(); + const port = await getPort({ port: 5432 }); + const image = 'postgis/postgis:14-3.5'; + + const pullStream = await docker.pull(image); + await new Promise((resolve, reject) => + docker.modem.followProgress(pullStream, (err) => (err ? reject(err) : resolve(err))) + ); + + pgContainer = await docker.createContainer({ + Image: image, + Env: ['POSTGRES_PASSWORD=postgres', 'POSTGRES_USER=postgres', 'POSTGRES_DB=postgres'], + name: `drizzle-integration-tests-${uuid()}`, + HostConfig: { + AutoRemove: true, + PortBindings: { + '5432/tcp': [{ HostPort: `${port}` }], + }, + }, + }); + + await pgContainer.start(); + + return { connectionString: `postgres://postgres:postgres@localhost:${port}/postgres`, container: pgContainer }; +} + +beforeAll(async () => { + const connectionString = process.env.PG_POSTGIS_CONNECTION_STRING ?? await createDockerDB(); + + const sleep = 1000; + let timeLeft = 20000; + let connected = false; + let lastError: unknown | undefined; + do { + try { + client = new Client(connectionString); + await client.connect(); + connected = true; + break; + } catch (e) { + lastError = e; + await new Promise((resolve) => setTimeout(resolve, sleep)); + timeLeft -= sleep; + } + } while (timeLeft > 0); + if (!connected) { + console.error('Cannot connect to Postgres'); + await client?.end().catch(console.error); + await pgContainer?.stop().catch(console.error); + throw lastError; + } +}); + +afterAll(async () => { + await client?.end().catch(console.error); + await pgContainer?.stop().catch(console.error); +}); + +test('geometry column', async () => { + await client.query('drop table if exists "users"'); + + const schema = { + users: pgTable('users', { + id: integer('id').primaryKey(), + geometry1: geometry('geometry1', { type: 'point' }), + }), + }; + + const { statements, sqlStatements } = await introspectPgToFile( + client, + schema, + 'geometry-column', + ['public'], + undefined, + undefined, + ignoreTables, + ); + + expect(statements.length).toBe(0); + expect(sqlStatements.length).toBe(0); +}); + +test('geometry column with default', async () => { + await client.query('drop table if exists "users"'); + + const schema = { + users: pgTable('users', { + id: integer('id').primaryKey(), + geometry1: geometry('geometry1', { type: 'point', mode: 'xy' }).default({ x: 1, y: 2 }), + geometry2: geometry('geometry2', { type: 'point', mode: 'tuple' }).default([3, 4]), + }), + }; + + const { statements, sqlStatements } = await introspectPgToFile( + client, + schema, + 'geometry-column-with-default', + ['public'], + undefined, + undefined, + ignoreTables, + ); + + expect(statements.length).toBe(0); + expect(sqlStatements.length).toBe(0); +}); diff --git a/drizzle-kit/tests/introspect/singlestore.test.ts b/drizzle-kit/tests/introspect/singlestore.test.ts index 71960c3f7..9a9d9e468 100644 --- a/drizzle-kit/tests/introspect/singlestore.test.ts +++ b/drizzle-kit/tests/introspect/singlestore.test.ts @@ -273,3 +273,22 @@ test('handle unsigned numerical types', async () => { expect(statements.length).toBe(0); expect(sqlStatements.length).toBe(0); }); + +test('introspect strings with empty string as default', async () => { + const schema = { + columns: singlestoreTable('columns', { + text: text('text').default(''), + varchar: varchar('varchar', { length: 255 }).default(''), + }), + }; + + const { statements, sqlStatements } = await introspectSingleStoreToFile( + client, + schema, + 'introspect-strings-with-empty-string-as-default', + 'drizzle', + ); + + expect(statements.length).toBe(0); + expect(sqlStatements.length).toBe(0); +}); diff --git a/drizzle-kit/tests/introspect/sqlite.test.ts b/drizzle-kit/tests/introspect/sqlite.test.ts index de13d4e81..70cba9538 100644 --- a/drizzle-kit/tests/introspect/sqlite.test.ts +++ b/drizzle-kit/tests/introspect/sqlite.test.ts @@ -75,6 +75,25 @@ test('instrospect strings with single quotes', async () => { expect(sqlStatements.length).toBe(0); }); +test('introspect strings with empty string as default', async () => { + const sqlite = new Database(':memory:'); + + const schema = { + columns: sqliteTable('columns', { + text: text('text').default(''), + }), + }; + + const { statements, sqlStatements } = await introspectSQLiteToFile( + sqlite, + schema, + 'introspect-strings-with-empty-string-as-default', + ); + + expect(statements.length).toBe(0); + expect(sqlStatements.length).toBe(0); +}); + test('introspect checks', async () => { const sqlite = new Database(':memory:'); diff --git a/drizzle-kit/tests/mysql-checks.test.ts b/drizzle-kit/tests/mysql-checks.test.ts index 82e7a5104..45c8146b8 100644 --- a/drizzle-kit/tests/mysql-checks.test.ts +++ b/drizzle-kit/tests/mysql-checks.test.ts @@ -289,3 +289,28 @@ test('create checks with same names', async (t) => { await expect(diffTestSchemasMysql({}, to, [])).rejects.toThrowError(); }); + +test('check with param', async (t) => { + const from = { + users: mysqlTable('users', { + id: serial('id').primaryKey(), + age: int('age'), + }), + }; + + const to = { + users: mysqlTable('users', { + id: serial('id').primaryKey(), + age: int('age'), + }, (table) => [ + check('name', sql`${table.age} > ${21}`), + ]), + }; + + const { sqlStatements } = await diffTestSchemasMysql(from, to, []); + + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toBe( + `ALTER TABLE \`users\` ADD CONSTRAINT \`name\` CHECK (\`users\`.\`age\` > 21);`, + ); +}); diff --git a/drizzle-kit/tests/mysql.test.ts b/drizzle-kit/tests/mysql.test.ts index 881b05ef7..6e826ea66 100644 --- a/drizzle-kit/tests/mysql.test.ts +++ b/drizzle-kit/tests/mysql.test.ts @@ -1,5 +1,6 @@ import { sql } from 'drizzle-orm'; import { + bigint, foreignKey, index, int, @@ -619,21 +620,21 @@ test('varchar and text default values escape single quotes', async (t) => { enum: mysqlEnum('enum', ["escape's quotes", "escape's quotes 2"]).default("escape's quotes"), text: text('text').default("escape's quotes"), varchar: varchar('varchar', { length: 255 }).default("escape's quotes"), + text2: text('text2').default(''), + varchar2: varchar('varchar2', { length: 255 }).default(''), }), }; const { sqlStatements } = await diffTestSchemasMysql(schema1, schem2, []); - expect(sqlStatements.length).toBe(3); - expect(sqlStatements[0]).toStrictEqual( + expect(sqlStatements.length).toBe(5); + expect(sqlStatements).toStrictEqual([ "ALTER TABLE `table` ADD `enum` enum('escape''s quotes','escape''s quotes 2') DEFAULT 'escape''s quotes';", - ); - expect(sqlStatements[1]).toStrictEqual( "ALTER TABLE `table` ADD `text` text DEFAULT ('escape''s quotes');", - ); - expect(sqlStatements[2]).toStrictEqual( "ALTER TABLE `table` ADD `varchar` varchar(255) DEFAULT 'escape''s quotes';", - ); + "ALTER TABLE `table` ADD `text2` text DEFAULT ('');", + "ALTER TABLE `table` ADD `varchar2` varchar(255) DEFAULT '';", + ]); }); test('composite primary key', async () => { @@ -861,3 +862,21 @@ test('optional db aliases (camel case)', async () => { expect(sqlStatements).toStrictEqual([st1, st2, st3, st4, st5, st6]); }); + +test('bigint with default', async (t) => { + const schema1 = {}; + + const schema2 = { + table: mysqlTable('table', { + bigint1: bigint('bigint1', { mode: 'bigint' }).default(0n), + bigint2: bigint('bigint2', { mode: 'bigint' }).default(10n), + }), + }; + + const { sqlStatements } = await diffTestSchemasMysql(schema1, schema2, []); + + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toStrictEqual( + 'CREATE TABLE \`table\` (\n\t\`bigint1\` bigint DEFAULT 0,\n\t\`bigint2\` bigint DEFAULT 10\n);\n', + ); +}); diff --git a/drizzle-kit/tests/pg-array.test.ts b/drizzle-kit/tests/pg-array.test.ts index e6c06d535..01f36d91b 100644 --- a/drizzle-kit/tests/pg-array.test.ts +++ b/drizzle-kit/tests/pg-array.test.ts @@ -2,6 +2,7 @@ import { bigint, boolean, date, + doublePrecision, integer, json, pgEnum, @@ -366,3 +367,24 @@ test('array #12: enum empty array default', async (t) => { }, }); }); + +test('array #13: double precision array', async (t) => { + const from = { + test: pgTable('test', { + id: serial('id').primaryKey(), + }), + }; + const to = { + test: pgTable('test', { + id: serial('id').primaryKey(), + double: doublePrecision('double').array(), + }), + }; + + const { sqlStatements } = await diffTestSchemas(from, to, []); + + expect(sqlStatements.length).toBe(1); + expect(sqlStatements).toStrictEqual([ + 'ALTER TABLE "test" ADD COLUMN "double" double precision[];', + ]); +}); diff --git a/drizzle-kit/tests/pg-checks.test.ts b/drizzle-kit/tests/pg-checks.test.ts index 8033aacef..d0a39cad9 100644 --- a/drizzle-kit/tests/pg-checks.test.ts +++ b/drizzle-kit/tests/pg-checks.test.ts @@ -280,3 +280,28 @@ test('create checks with same names', async (t) => { await expect(diffTestSchemas({}, to, [])).rejects.toThrowError(); }); + +test('check with param', async (t) => { + const from = { + users: pgTable('users', { + id: serial('id').primaryKey(), + age: integer('age'), + }), + }; + + const to = { + users: pgTable('users', { + id: serial('id').primaryKey(), + age: integer('age'), + }, (table) => [ + check('name', sql`${table.age} > ${21}`), + ]), + }; + + const { sqlStatements } = await diffTestSchemas(from, to, []); + + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toBe( + `ALTER TABLE "users" ADD CONSTRAINT "name" CHECK ("users"."age" > 21);`, + ); +}); diff --git a/drizzle-kit/tests/pg-columns.test.ts b/drizzle-kit/tests/pg-columns.test.ts index ddd744a81..290488317 100644 --- a/drizzle-kit/tests/pg-columns.test.ts +++ b/drizzle-kit/tests/pg-columns.test.ts @@ -1,4 +1,4 @@ -import { integer, pgTable, primaryKey, serial, text, uuid, varchar } from 'drizzle-orm/pg-core'; +import { bigint, bit, integer, pgTable, primaryKey, serial, text, uuid, varchar } from 'drizzle-orm/pg-core'; import { expect, test } from 'vitest'; import { diffTestSchemas } from './schemaDiffer'; @@ -469,16 +469,55 @@ test('varchar and text default values escape single quotes', async (t) => { id: serial('id').primaryKey(), text: text('text').default("escape's quotes"), varchar: varchar('varchar').default("escape's quotes"), + text2: text('text2').default(''), + varchar2: varchar('varchar2', { length: 200 }).default(''), }), }; const { sqlStatements } = await diffTestSchemas(schema1, schem2, []); - expect(sqlStatements.length).toBe(2); - expect(sqlStatements[0]).toStrictEqual( + expect(sqlStatements.length).toBe(4); + expect(sqlStatements).toStrictEqual([ 'ALTER TABLE "table" ADD COLUMN "text" text DEFAULT \'escape\'\'s quotes\';', - ); - expect(sqlStatements[1]).toStrictEqual( 'ALTER TABLE "table" ADD COLUMN "varchar" varchar DEFAULT \'escape\'\'s quotes\';', + 'ALTER TABLE "table" ADD COLUMN "text2" text DEFAULT \'\';', + 'ALTER TABLE "table" ADD COLUMN "varchar2" varchar(200) DEFAULT \'\';', + ]); +}); + +test('bit type', async (t) => { + const schema1 = {}; + + const schema2 = { + table: pgTable('table', { + id: serial('id').primaryKey(), + bit: bit('bit', { dimensions: 10 }), + }), + }; + + const { sqlStatements } = await diffTestSchemas(schema1, schema2, []); + + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toStrictEqual( + 'CREATE TABLE "table" (\n\t"id" serial PRIMARY KEY NOT NULL,\n\t"bit" bit(10)\n);\n', + ); +}); + +test('bigint with default', async (t) => { + const schema1 = {}; + + const schema2 = { + table: pgTable('table', { + id: serial('id').primaryKey(), + bigint1: bigint('bigint1', { mode: 'bigint' }).default(0n), + bigint2: bigint('bigint2', { mode: 'bigint' }).default(10n), + }), + }; + + const { sqlStatements } = await diffTestSchemas(schema1, schema2, []); + + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toStrictEqual( + 'CREATE TABLE "table" (\n\t"id" serial PRIMARY KEY NOT NULL,\n\t"bigint1" bigint DEFAULT 0,\n\t"bigint2" bigint DEFAULT 10\n);\n', ); }); diff --git a/drizzle-kit/tests/pg-identity.test.ts b/drizzle-kit/tests/pg-identity.test.ts index efb481da3..fc46bdd49 100644 --- a/drizzle-kit/tests/pg-identity.test.ts +++ b/drizzle-kit/tests/pg-identity.test.ts @@ -1,4 +1,4 @@ -import { integer, pgSequence, pgTable } from 'drizzle-orm/pg-core'; +import { integer, pgSequence, pgTable, serial } from 'drizzle-orm/pg-core'; import { expect, test } from 'vitest'; import { diffTestSchemas } from './schemaDiffer'; @@ -445,3 +445,27 @@ test('alter identity from a column - always to by default', async () => { 'ALTER TABLE "users" ALTER COLUMN "id" SET CACHE 10;', ]); }); + +test('switch serial to integer generated always as identity', async (t) => { + const schema1 = { + table: pgTable('table', { + id: serial('id').primaryKey(), + }), + }; + + const schema2 = { + table: pgTable('table', { + id: integer('id').generatedAlwaysAsIdentity().primaryKey(), + }), + }; + + const { sqlStatements } = await diffTestSchemas(schema1, schema2, []); + + expect(sqlStatements.length).toBe(4); + expect(sqlStatements).toStrictEqual([ + 'ALTER TABLE "table" ALTER COLUMN "id" DROP DEFAULT;', + 'DROP SEQUENCE "table_id_seq";', + 'ALTER TABLE "table" ALTER COLUMN "id" ADD GENERATED ALWAYS AS IDENTITY (sequence name "table_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1);', + 'SELECT setval(pg_get_serial_sequence(\'table\', \'id\'), coalesce(max("id"), 0) + 1, false) FROM "table";', + ]); +}); diff --git a/drizzle-kit/tests/push/pg.test.ts b/drizzle-kit/tests/push/pg.test.ts index a7bed413d..fccf9acc1 100644 --- a/drizzle-kit/tests/push/pg.test.ts +++ b/drizzle-kit/tests/push/pg.test.ts @@ -29,6 +29,7 @@ import { text, time, timestamp, + unique, uniqueIndex, uuid, varchar, @@ -1956,6 +1957,7 @@ test('add identity to column - few params', async () => { expect(statements).toStrictEqual([ { + changedSerialToIntegerIdentity: false, columnName: 'id', identity: 'custom_name;byDefault;1;2147483647;1;1;1;false', schema: '', @@ -1963,6 +1965,7 @@ test('add identity to column - few params', async () => { type: 'alter_table_alter_column_set_identity', }, { + changedSerialToIntegerIdentity: false, columnName: 'id1', identity: 'custom_name1;always;1;2147483647;4;1;1;false', schema: '', @@ -4339,3 +4342,142 @@ test('alter inherit in role', async (t) => { await client.query(st); } }); + +test('switch serial to integer generated always as identity', async (t) => { + const client = new PGlite(); + + const schema1 = { + table: pgTable('table', { + id: serial('id').primaryKey(), + }), + }; + + const schema2 = { + table: pgTable('table', { + id: integer('id').generatedAlwaysAsIdentity().primaryKey(), + }), + }; + + const { sqlStatements } = await diffTestSchemasPush( + client, + schema1, + schema2, + [], + false, + ['public'], + undefined, + ); + + expect(sqlStatements).toStrictEqual([ + 'ALTER TABLE "table" ALTER COLUMN "id" DROP DEFAULT;', + 'DROP SEQUENCE "table_id_seq";', + 'ALTER TABLE "table" ALTER COLUMN "id" ADD GENERATED ALWAYS AS IDENTITY (sequence name "table_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1);', + 'SELECT setval(pg_get_serial_sequence(\'table\', \'id\'), coalesce(max("id"), 0) + 1, false) FROM "table";', + ]); + + for (const st of sqlStatements) { + await client.query(st); + } +}); + +test('bigint generated always as identity', async (t) => { + const client = new PGlite(); + + const schema1 = {}; + + const schema2 = { + table: pgTable('table', { + id: bigint('id', { mode: 'bigint' }).generatedAlwaysAsIdentity().primaryKey(), + }), + }; + + const { sqlStatements } = await diffTestSchemasPush( + client, + schema1, + schema2, + [], + false, + ['public'], + undefined, + ); + + expect(sqlStatements).toStrictEqual([ + 'CREATE TABLE "table" (\n\t"id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "table_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1)\n);\n', + ]); + + for (const st of sqlStatements) { + await client.query(st); + } +}); + +test("identity doesn't change when table name has non-alphanumeric characters", async () => { + const client = new PGlite(); + + const schema1 = {}; + + const schema2 = { + table1: pgTable('table-1', { + id: integer('id').generatedAlwaysAsIdentity().primaryKey(), + }), + table2: pgTable('table-2', { + id: integer('id').generatedByDefaultAsIdentity().primaryKey(), + }), + }; + + await diffTestSchemasPush( + client, + schema1, + schema2, + [], + false, + ['public'], + undefined, + ); + const { sqlStatements: sqlStatements2 } = await diffTestSchemasPush( + client, + schema2, + schema2, + [], + false, + ['public'], + undefined, + ); + + expect(sqlStatements2).toStrictEqual([]); +}); + +test("shouldn't drop unique constraint", async () => { + const client = new PGlite(); + + const schema1 = { + table: pgTable('table', { + id: integer('id').primaryKey(), + u2: text('u2'), + u1: text('u1'), + }, (t) => [ + unique('table_u1_u2_uni').on(t.u1, t.u2), + ]), + }; + + const schema2 = { + table: pgTable('table', { + id: integer('id').primaryKey(), + u2: text('u2'), + u1: text('u1'), + }, (t) => [ + unique('table_u1_u2_uni').on(t.u1, t.u2), + ]), + }; + + const { sqlStatements } = await diffTestSchemasPush( + client, + schema1, + schema2, + [], + false, + ['public'], + undefined, + ); + + expect(sqlStatements).toStrictEqual([]); +}); diff --git a/drizzle-kit/tests/schemaDiffer.ts b/drizzle-kit/tests/schemaDiffer.ts index 256288c24..e8bb73f59 100644 --- a/drizzle-kit/tests/schemaDiffer.ts +++ b/drizzle-kit/tests/schemaDiffer.ts @@ -22,6 +22,7 @@ import { SingleStoreSchema, SingleStoreTable } from 'drizzle-orm/singlestore-cor import { SQLiteTable, SQLiteView } from 'drizzle-orm/sqlite-core'; import * as fs from 'fs'; import { Connection } from 'mysql2/promise'; +import { Client as NodePgClient } from 'pg'; import { libSqlLogSuggestionsAndReturn } from 'src/cli/commands/libSqlPushUtils'; import { columnsResolver, @@ -2296,24 +2297,27 @@ export const diffTestSchemasLibSQL = async ( // --- Introspect to file helpers --- export const introspectPgToFile = async ( - client: PGlite, + client: PGlite | NodePgClient, initSchema: PostgresSchema, testName: string, schemas: string[] = ['public'], entities?: Entities, casing?: CasingType | undefined, + ignoreTables?: string[] | undefined, ) => { + const execute: (sql: string, params?: any[]) => Promise = (sql, params) => (client as any).query(sql, params); + // put in db const { sqlStatements } = await applyPgDiffs(initSchema, casing); for (const st of sqlStatements) { - await client.query(st); + await execute(st); } // introspect to schema const introspectedSchema = await fromDatabase( { query: async (query: string, values?: any[] | undefined) => { - const res = await client.query(query, values); + const res = await execute(query, values); return res.rows as any[]; }, }, @@ -2322,6 +2326,13 @@ export const introspectPgToFile = async ( entities, ); + if (ignoreTables && ignoreTables.length > 0) { + for (const table of ignoreTables) { + delete introspectedSchema.tables[table]; + delete introspectedSchema.views[table]; + } + } + const { version: initV, dialect: initD, ...initRest } = introspectedSchema; const initSch = { diff --git a/drizzle-kit/tests/singlestore.test.ts b/drizzle-kit/tests/singlestore.test.ts index dca99ad2d..75d592d6f 100644 --- a/drizzle-kit/tests/singlestore.test.ts +++ b/drizzle-kit/tests/singlestore.test.ts @@ -1,5 +1,6 @@ import { sql } from 'drizzle-orm'; import { + bigint, index, int, json, @@ -976,3 +977,21 @@ test('set not null + rename table on table with indexes', async () => { 'ALTER TABLE `__new_table1` RENAME TO `table1`;', ); }); + +test('bigint with default', async (t) => { + const schema1 = {}; + + const schema2 = { + table: singlestoreTable('table', { + bigint1: bigint('bigint1', { mode: 'bigint' }).default(0n), + bigint2: bigint('bigint2', { mode: 'bigint' }).default(10n), + }), + }; + + const { sqlStatements } = await diffTestSchemasSingleStore(schema1, schema2, []); + + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toStrictEqual( + 'CREATE TABLE \`table\` (\n\t\`bigint1\` bigint DEFAULT 0,\n\t\`bigint2\` bigint DEFAULT 10\n);\n', + ); +}); diff --git a/drizzle-kit/tests/sqlite-checks.test.ts b/drizzle-kit/tests/sqlite-checks.test.ts index d1824e441..a326efeab 100644 --- a/drizzle-kit/tests/sqlite-checks.test.ts +++ b/drizzle-kit/tests/sqlite-checks.test.ts @@ -306,3 +306,33 @@ test('create checks with same names', async (t) => { await expect(diffTestSchemasSqlite({}, to, [])).rejects.toThrowError(); }); + +test('check with param', async (t) => { + const from = { + users: sqliteTable('users', { + id: int('id').primaryKey(), + age: int('age'), + }), + }; + + const to = { + users: sqliteTable('users', { + id: int('id').primaryKey(), + age: int('age'), + }, (table) => [ + check('name', sql`${table.age} > ${21}`), + ]), + }; + + const { sqlStatements } = await diffTestSchemasSqlite(from, to, []); + + expect(sqlStatements.length).toBe(6); + expect(sqlStatements).toStrictEqual([ + 'PRAGMA foreign_keys=OFF;', + 'CREATE TABLE `__new_users` (\n\t`id` integer PRIMARY KEY NOT NULL,\n\t`age` integer,\n\tCONSTRAINT "name" CHECK("__new_users"."age" > 21)\n);\n', + 'INSERT INTO `__new_users`("id", "age") SELECT "id", "age" FROM `users`;', + 'DROP TABLE `users`;', + 'ALTER TABLE `__new_users` RENAME TO `users`;', + 'PRAGMA foreign_keys=ON;', + ]); +}); diff --git a/drizzle-kit/tests/sqlite-columns.test.ts b/drizzle-kit/tests/sqlite-columns.test.ts index 0cb34c220..d52ab1570 100644 --- a/drizzle-kit/tests/sqlite-columns.test.ts +++ b/drizzle-kit/tests/sqlite-columns.test.ts @@ -1,5 +1,6 @@ import { AnySQLiteColumn, + blob, foreignKey, index, int, @@ -1037,13 +1038,33 @@ test('text default values escape single quotes', async (t) => { table: sqliteTable('table', { id: integer('id').primaryKey(), text: text('text').default("escape's quotes"), + text2: text('text2').default(''), }), }; const { sqlStatements } = await diffTestSchemasSqlite(schema1, schem2, []); + expect(sqlStatements.length).toBe(2); + expect(sqlStatements).toStrictEqual([ + "ALTER TABLE `table` ADD `text` text DEFAULT 'escape''s quotes';", + "ALTER TABLE `table` ADD `text2` text DEFAULT '';", + ]); +}); + +test('bigint with default', async (t) => { + const schema1 = {}; + + const schema2 = { + table: sqliteTable('table', { + bigint1: blob('bigint1', { mode: 'bigint' }).default(0n), + bigint2: blob('bigint2', { mode: 'bigint' }).default(10n), + }), + }; + + const { sqlStatements } = await diffTestSchemasSqlite(schema1, schema2, []); + expect(sqlStatements.length).toBe(1); expect(sqlStatements[0]).toStrictEqual( - "ALTER TABLE `table` ADD `text` text DEFAULT 'escape''s quotes';", + 'CREATE TABLE \`table\` (\n\t\`bigint1\` blob DEFAULT 0,\n\t\`bigint2\` blob DEFAULT 10\n);\n', ); });