diff --git a/drizzle-kit/src/cli/commands/pgIntrospect.ts b/drizzle-kit/src/cli/commands/pgIntrospect.ts index 2d3fd75ce..02867fae9 100644 --- a/drizzle-kit/src/cli/commands/pgIntrospect.ts +++ b/drizzle-kit/src/cli/commands/pgIntrospect.ts @@ -1,7 +1,7 @@ import { renderWithTask } from 'hanji'; import { Minimatch } from 'minimatch'; import { originUUID } from '../../global'; -import type { PgSchema } from '../../serializer/pgSchema'; +import type { PgSchema, PgSchemaInternal } from '../../serializer/pgSchema'; import { fromDatabase } from '../../serializer/pgSerializer'; import type { DB } from '../../utils'; import { Entities } from '../validations/cli'; @@ -12,6 +12,7 @@ export const pgPushIntrospect = async ( filters: string[], schemaFilters: string[], entities: Entities, + tsSchema?: PgSchemaInternal, ) => { const matchers = filters.map((it) => { return new Minimatch(it); @@ -45,7 +46,7 @@ export const pgPushIntrospect = async ( ); const res = await renderWithTask( progress, - fromDatabase(db, filter, schemaFilters, entities), + fromDatabase(db, filter, schemaFilters, entities, undefined, tsSchema), ); const schema = { id: originUUID, prevId: '', ...res } as PgSchema; diff --git a/drizzle-kit/src/cli/commands/pgPushUtils.ts b/drizzle-kit/src/cli/commands/pgPushUtils.ts index b53fec3e7..05322f738 100644 --- a/drizzle-kit/src/cli/commands/pgPushUtils.ts +++ b/drizzle-kit/src/cli/commands/pgPushUtils.ts @@ -250,7 +250,7 @@ export const pgSuggestions = async (db: DB, statements: JsonStatement[]) => { } } } - const stmnt = fromJson([statement], 'postgresql'); + const stmnt = fromJson([statement], 'postgresql', 'push'); if (typeof stmnt !== 'undefined') { statementsToExecute.push(...stmnt); } diff --git a/drizzle-kit/src/introspect-mysql.ts b/drizzle-kit/src/introspect-mysql.ts index c15fea937..ebf30f70d 100644 --- a/drizzle-kit/src/introspect-mysql.ts +++ b/drizzle-kit/src/introspect-mysql.ts @@ -14,6 +14,7 @@ import { UniqueConstraint, } from './serializer/mysqlSchema'; import { indexName } from './serializer/mysqlSerializer'; +import { unescapeSingleQuotes } from './utils'; // time precision to fsp // {mode: "string"} for timestamp by default @@ -679,8 +680,9 @@ const column = ( ) } })`; + const mappedDefaultValue = mapColumnDefault(defaultValue, isExpression); out += defaultValue - ? `.default(${mapColumnDefault(defaultValue, isExpression)})` + ? `.default(${isExpression ? mappedDefaultValue : unescapeSingleQuotes(mappedDefaultValue, true)})` : ''; return out; } @@ -787,10 +789,15 @@ const column = ( } if (lowered.startsWith('enum')) { - const values = lowered.substring('enum'.length + 1, lowered.length - 1); + const values = lowered + .substring('enum'.length + 1, lowered.length - 1) + .split(',') + .map((v) => unescapeSingleQuotes(v, true)) + .join(','); let out = `${casing(name)}: mysqlEnum(${dbColumnName({ name, casing: rawCasing, withMode: true })}[${values}])`; + const mappedDefaultValue = mapColumnDefault(defaultValue, isExpression); out += defaultValue - ? `.default(${mapColumnDefault(defaultValue, isExpression)})` + ? `.default(${isExpression ? mappedDefaultValue : unescapeSingleQuotes(mappedDefaultValue, true)})` : ''; return out; } diff --git a/drizzle-kit/src/introspect-pg.ts b/drizzle-kit/src/introspect-pg.ts index ed26e8117..9c9383ebe 100644 --- a/drizzle-kit/src/introspect-pg.ts +++ b/drizzle-kit/src/introspect-pg.ts @@ -11,7 +11,6 @@ import { import './@types/utils'; import { toCamelCase } from 'drizzle-orm/casing'; import { Casing } from './cli/validations/common'; -import { vectorOps } from './extensions/vector'; import { assertUnreachable } from './global'; import { CheckConstraint, @@ -25,6 +24,7 @@ import { UniqueConstraint, } from './serializer/pgSchema'; import { indexName } from './serializer/pgSerializer'; +import { unescapeSingleQuotes } from './utils'; const pgImportsList = new Set([ 'pgTable', @@ -436,7 +436,7 @@ export const schemaToTypeScript = (schema: PgSchemaInternal, casing: Casing) => const func = enumSchema ? `${enumSchema}.enum` : 'pgEnum'; const values = Object.values(it.values) - .map((it) => `'${it}'`) + .map((it) => `'${unescapeSingleQuotes(it, false)}'`) .join(', '); return `export const ${withCasing(paramName, casing)} = ${func}("${it.name}", [${values}])\n`; }) @@ -690,7 +690,9 @@ const mapDefault = ( } if (enumTypes.has(`${typeSchema}.${type.replace('[]', '')}`)) { - return typeof defaultValue !== 'undefined' ? `.default(${mapColumnDefault(defaultValue, isExpression)})` : ''; + return typeof defaultValue !== 'undefined' + ? `.default(${mapColumnDefault(unescapeSingleQuotes(defaultValue, true), isExpression)})` + : ''; } if (lowered.startsWith('integer')) { @@ -737,18 +739,20 @@ const mapDefault = ( if (lowered.startsWith('timestamp')) { return defaultValue === 'now()' ? '.defaultNow()' - : defaultValue === 'CURRENT_TIMESTAMP' - ? '.default(sql`CURRENT_TIMESTAMP`)' - : defaultValue + : /^'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?([+-]\d{2}(:\d{2})?)?'$/.test(defaultValue) // Matches 'YYYY-MM-DD HH:MI:SS', 'YYYY-MM-DD HH:MI:SS.FFFFFF', 'YYYY-MM-DD HH:MI:SS+TZ', 'YYYY-MM-DD HH:MI:SS.FFFFFF+TZ' and 'YYYY-MM-DD HH:MI:SS+HH:MI' ? `.default(${mapColumnDefault(defaultValue, isExpression)})` + : defaultValue + ? `.default(sql\`${defaultValue}\`)` : ''; } if (lowered.startsWith('time')) { return defaultValue === 'now()' ? '.defaultNow()' - : defaultValue + : /^'\d{2}:\d{2}(:\d{2})?(\.\d+)?'$/.test(defaultValue) // Matches 'HH:MI', 'HH:MI:SS' and 'HH:MI:SS.FFFFFF' ? `.default(${mapColumnDefault(defaultValue, isExpression)})` + : defaultValue + ? `.default(sql\`${defaultValue}\`)` : ''; } @@ -759,15 +763,17 @@ const mapDefault = ( if (lowered === 'date') { return defaultValue === 'now()' ? '.defaultNow()' - : defaultValue === 'CURRENT_DATE' - ? `.default(sql\`${defaultValue}\`)` - : defaultValue + : /^'\d{4}-\d{2}-\d{2}'$/.test(defaultValue) // Matches 'YYYY-MM-DD' ? `.default(${defaultValue})` + : defaultValue + ? `.default(sql\`${defaultValue}\`)` : ''; } if (lowered.startsWith('text')) { - return typeof defaultValue !== 'undefined' ? `.default(${mapColumnDefault(defaultValue, isExpression)})` : ''; + return typeof defaultValue !== 'undefined' + ? `.default(${mapColumnDefault(unescapeSingleQuotes(defaultValue, true), isExpression)})` + : ''; } if (lowered.startsWith('jsonb')) { @@ -801,7 +807,9 @@ const mapDefault = ( } if (lowered.startsWith('varchar')) { - return typeof defaultValue !== 'undefined' ? `.default(${mapColumnDefault(defaultValue, isExpression)})` : ''; + return typeof defaultValue !== 'undefined' + ? `.default(${mapColumnDefault(unescapeSingleQuotes(defaultValue, true), isExpression)})` + : ''; } if (lowered.startsWith('point')) { @@ -821,7 +829,9 @@ const mapDefault = ( } if (lowered.startsWith('char')) { - return typeof defaultValue !== 'undefined' ? `.default(${mapColumnDefault(defaultValue, isExpression)})` : ''; + return typeof defaultValue !== 'undefined' + ? `.default(${mapColumnDefault(unescapeSingleQuotes(defaultValue, true), isExpression)})` + : ''; } return ''; @@ -1219,7 +1229,11 @@ const createTableIndexes = (tableName: string, idxs: Index[], casing: Casing): s } else { return `table.${withCasing(it.expression, casing)}${it.asc ? '.asc()' : '.desc()'}${ it.nulls === 'first' ? '.nullsFirst()' : '.nullsLast()' - }${it.opclass && vectorOps.includes(it.opclass) ? `.op("${it.opclass}")` : ''}`; + }${ + it.opclass + ? `.op("${it.opclass}")` + : '' + }`; } }) .join(', ') diff --git a/drizzle-kit/src/introspect-sqlite.ts b/drizzle-kit/src/introspect-sqlite.ts index e21f2a5c4..464a32aa3 100644 --- a/drizzle-kit/src/introspect-sqlite.ts +++ b/drizzle-kit/src/introspect-sqlite.ts @@ -272,10 +272,8 @@ const mapColumnDefault = (defaultValue: any) => { if ( typeof defaultValue === 'string' - && defaultValue.startsWith("'") - && defaultValue.endsWith("'") ) { - return defaultValue.substring(1, defaultValue.length - 1); + return defaultValue.substring(1, defaultValue.length - 1).replaceAll('"', '\\"').replaceAll("''", "'"); } return defaultValue; diff --git a/drizzle-kit/src/serializer/mysqlSerializer.ts b/drizzle-kit/src/serializer/mysqlSerializer.ts index 25ca1d596..aaa1acb82 100644 --- a/drizzle-kit/src/serializer/mysqlSerializer.ts +++ b/drizzle-kit/src/serializer/mysqlSerializer.ts @@ -26,13 +26,20 @@ import { UniqueConstraint, View, } from '../serializer/mysqlSchema'; -import type { DB } from '../utils'; +import { type DB, escapeSingleQuotes } from '../utils'; import { getColumnCasing, sqlToStr } from './utils'; export const indexName = (tableName: string, columns: string[]) => { return `${tableName}_${columns.join('_')}_index`; }; +const handleEnumType = (type: string) => { + let str = type.split('(')[1]; + str = str.substring(0, str.length - 1); + const values = str.split(',').map((v) => `'${escapeSingleQuotes(v.substring(1, v.length - 1))}'`); + return `enum(${values.join(',')})`; +}; + export const generateMySqlSnapshot = ( tables: AnyMySqlTable[], views: MySqlView[], @@ -68,7 +75,8 @@ export const generateMySqlSnapshot = ( columns.forEach((column) => { const name = getColumnCasing(column, casing); const notNull: boolean = column.notNull; - const sqlTypeLowered = column.getSQLType().toLowerCase(); + const sqlType = column.getSQLType(); + const sqlTypeLowered = sqlType.toLowerCase(); const autoIncrement = typeof (column as any).autoIncrement === 'undefined' ? false : (column as any).autoIncrement; @@ -77,7 +85,7 @@ export const generateMySqlSnapshot = ( const columnToSet: Column = { name, - type: column.getSQLType(), + type: sqlType.startsWith('enum') ? handleEnumType(sqlType) : sqlType, primaryKey: false, // If field is autoincrement it's notNull by default // notNull: autoIncrement ? true : notNull, @@ -141,7 +149,7 @@ export const generateMySqlSnapshot = ( columnToSet.default = sqlToStr(column.default, casing); } else { if (typeof column.default === 'string') { - columnToSet.default = `'${column.default}'`; + columnToSet.default = `'${escapeSingleQuotes(column.default)}'`; } else { if (sqlTypeLowered === 'json') { columnToSet.default = `'${JSON.stringify(column.default)}'`; @@ -544,9 +552,9 @@ function clearDefaults(defaultValue: any, collate: string) { .substring(collate.length, defaultValue.length) .replace(/\\/g, ''); if (resultDefault.startsWith("'") && resultDefault.endsWith("'")) { - return `('${resultDefault.substring(1, resultDefault.length - 1)}')`; + return `('${escapeSingleQuotes(resultDefault.substring(1, resultDefault.length - 1))}')`; } else { - return `'${resultDefault}'`; + return `'${escapeSingleQuotes(resultDefault.substring(1, resultDefault.length - 1))}'`; } } else { return `(${resultDefault})`; @@ -665,14 +673,14 @@ export const fromDatabase = async ( } const newColumn: Column = { - default: columnDefault === null + default: columnDefault === null || columnDefault === undefined ? undefined : /^-?[\d.]+(?:e-?\d+)?$/.test(columnDefault) && !['decimal', 'char', 'varchar'].some((type) => columnType.startsWith(type)) ? Number(columnDefault) : isDefaultAnExpression ? clearDefaults(columnDefault, collation) - : `'${columnDefault}'`, + : `'${escapeSingleQuotes(columnDefault)}'`, autoincrement: isAutoincrement, name: columnName, type: changedType, diff --git a/drizzle-kit/src/serializer/pgSchema.ts b/drizzle-kit/src/serializer/pgSchema.ts index 50d712dc4..d7604d645 100644 --- a/drizzle-kit/src/serializer/pgSchema.ts +++ b/drizzle-kit/src/serializer/pgSchema.ts @@ -1,4 +1,3 @@ -import { vectorOps } from 'src/extensions/vector'; import { mapValues, originUUID, snapshotVersion } from '../global'; import { any, array, boolean, enum as enumType, literal, number, object, record, string, TypeOf, union } from 'zod'; @@ -240,6 +239,7 @@ export const policy = object({ using: string().optional(), withCheck: string().optional(), on: string().optional(), + schema: string().optional(), }).strict(); export const policySquashed = object({ @@ -554,10 +554,7 @@ export const PgSquasher = { return `${idx.name};${ idx.columns .map( - (c) => - `${c.expression}--${c.isExpression}--${c.asc}--${c.nulls}--${ - c.opclass && vectorOps.includes(c.opclass) ? c.opclass : '' - }`, + (c) => `${c.expression}--${c.isExpression}--${c.asc}--${c.nulls}--${c.opclass ? c.opclass : ''}`, ) .join(',,') };${idx.isUnique};${idx.concurrently};${idx.method};${idx.where};${JSON.stringify(idx.with)}`; @@ -657,6 +654,16 @@ export const PgSquasher = { squashPolicyPush: (policy: Policy) => { return `${policy.name}--${policy.as}--${policy.for}--${policy.to?.join(',')}--${policy.on}`; }, + unsquashPolicyPush: (policy: string): Policy => { + const splitted = policy.split('--'); + return { + name: splitted[0], + as: splitted[1] as Policy['as'], + for: splitted[2] as Policy['for'], + to: splitted[3].split(','), + on: splitted[4] !== 'undefined' ? splitted[4] : undefined, + }; + }, squashPK: (pk: PrimaryKey) => { return `${pk.columns.join(',')};${pk.name}`; }, diff --git a/drizzle-kit/src/serializer/pgSerializer.ts b/drizzle-kit/src/serializer/pgSerializer.ts index c6f6c0391..b0faa5ea8 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, isPgArrayType } from '../utils'; +import { type DB, escapeSingleQuotes, isPgArrayType } from '../utils'; import { getColumnCasing, sqlToStr } from './utils'; export const indexName = (tableName: string, columns: string[]) => { @@ -241,7 +241,7 @@ export const generatePgSnapshot = ( columnToSet.default = sqlToStr(column.default, casing); } else { if (typeof column.default === 'string') { - columnToSet.default = `'${column.default}'`; + columnToSet.default = `'${escapeSingleQuotes(column.default)}'`; } else { if (sqlTypeLowered === 'jsonb' || sqlTypeLowered === 'json') { columnToSet.default = `'${JSON.stringify(column.default)}'::${sqlTypeLowered}`; @@ -652,6 +652,7 @@ export const generatePgSnapshot = ( } else { policiesToReturn[policy.name] = { ...mappedPolicy, + schema: tableConfig.schema ?? 'public', on: `"${tableConfig.schema ?? 'public'}"."${tableConfig.name}"`, }; } @@ -972,9 +973,11 @@ export const fromDatabase = async ( count: number, status: IntrospectStatus, ) => void, + tsSchema?: PgSchemaInternal, ): Promise => { const result: Record = {}; const views: Record = {}; + const policies: Record = {}; const internals: PgKitInternals = { tables: {} }; const where = schemaFilters.map((t) => `n.nspname = '${t}'`).join(' or '); @@ -1134,7 +1137,9 @@ WHERE } } - const wherePolicies = schemaFilters + const schemasForLinkedPoliciesInSchema = Object.values(tsSchema?.policies ?? {}).map((it) => it.schema!); + + const wherePolicies = [...schemaFilters, ...schemasForLinkedPoliciesInSchema] .map((t) => `schemaname = '${t}'`) .join(' or '); @@ -1171,6 +1176,16 @@ WHERE [dbPolicy.name]: { ...rest, to: parsedTo, withCheck: parsedWithCheck, using: parsedUsing } as Policy, }; } + + if (tsSchema?.policies[dbPolicy.name]) { + policies[dbPolicy.name] = { + ...rest, + to: parsedTo, + withCheck: parsedWithCheck, + using: parsedUsing, + on: tsSchema?.policies[dbPolicy.name].on, + } as Policy; + } } if (progressCallback) { @@ -1907,7 +1922,7 @@ WHERE schemas: schemasObject, sequences: sequencesToReturn, roles: rolesToReturn, - policies: {}, + policies, views: views, _meta: { schemas: {}, @@ -1922,11 +1937,13 @@ const defaultForColumn = (column: any, internals: PgKitInternals, tableName: str const columnName = column.column_name; const isArray = internals?.tables[tableName]?.columns[columnName]?.isArray ?? false; - if (column.column_default === null) { - return undefined; - } - - if (column.data_type === 'serial' || column.data_type === 'smallserial' || column.data_type === 'bigserial') { + if ( + column.column_default === null + || column.column_default === undefined + || column.data_type === 'serial' + || column.data_type === 'smallserial' + || column.data_type === 'bigserial' + ) { return undefined; } diff --git a/drizzle-kit/src/serializer/sqliteSerializer.ts b/drizzle-kit/src/serializer/sqliteSerializer.ts index 1ba24b69c..107a1b292 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 type { SQLiteDB } from '../utils'; +import { escapeSingleQuotes, type SQLiteDB } from '../utils'; import { getColumnCasing, sqlToStr } from './utils'; export const generateSqliteSnapshot = ( @@ -90,7 +90,7 @@ export const generateSqliteSnapshot = ( columnToSet.default = sqlToStr(column.default, casing); } else { columnToSet.default = typeof column.default === 'string' - ? `'${column.default}'` + ? `'${escapeSingleQuotes(column.default)}'` : 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 9d1479653..81f04f10e 100644 --- a/drizzle-kit/src/sqlgenerator.ts +++ b/drizzle-kit/src/sqlgenerator.ts @@ -4041,17 +4041,6 @@ convertors.push(new SingleStoreAlterTableCreateCompositePrimaryKeyConvertor()); convertors.push(new SingleStoreAlterTableAddPk()); convertors.push(new SingleStoreAlterTableAlterCompositePrimaryKeyConvertor()); -export function fromJson( - statements: JsonStatement[], - dialect: Exclude, -): string[]; -export function fromJson( - statements: JsonStatement[], - dialect: 'sqlite' | 'turso', - action?: 'push', - json2?: SQLiteSchemaSquashed, -): string[]; - export function fromJson( statements: JsonStatement[], dialect: Dialect, diff --git a/drizzle-kit/src/utils.ts b/drizzle-kit/src/utils.ts index 71454550e..559153c38 100644 --- a/drizzle-kit/src/utils.ts +++ b/drizzle-kit/src/utils.ts @@ -1,9 +1,11 @@ + import type { RunResult } from 'better-sqlite3'; import chalk from 'chalk'; import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import { parse } from 'url'; import type { NamedWithSchema } from './cli/commands/migrate'; +import { CasingType } from './cli/validations/common'; import { info } from './cli/views'; import { assertUnreachable, snapshotVersion } from './global'; import type { Dialect } from './schemaValidator'; @@ -359,3 +361,12 @@ export function findAddedAndRemoved(columnNames1: string[], columnNames2: string return { addedColumns, removedColumns }; } + +export function escapeSingleQuotes(str: string) { + return str.replace(/'/g, "''"); +} + +export function unescapeSingleQuotes(str: string, ignoreFirstAndLastChar: boolean) { + const regex = ignoreFirstAndLastChar ? /(? { expect(statements.length).toBe(0); expect(sqlStatements.length).toBe(0); }); + +test('instrospect strings with single quotes', async () => { + const schema = { + columns: mysqlTable('columns', { + enum: mysqlEnum('my_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 " '), + }), + }; + + const { statements, sqlStatements } = await introspectMySQLToFile( + client, + schema, + 'introspect-strings-with-single-quotes', + '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 6762ef27a..1d9f0f18c 100644 --- a/drizzle-kit/tests/introspect/pg.test.ts +++ b/drizzle-kit/tests/introspect/pg.test.ts @@ -255,8 +255,12 @@ test('instrospect all column types', async () => { time2: time('time2').defaultNow(), timestamp1: timestamp('timestamp1', { withTimezone: true, precision: 6 }).default(new Date()), timestamp2: timestamp('timestamp2', { withTimezone: true, precision: 6 }).defaultNow(), + timestamp3: timestamp('timestamp3', { withTimezone: true, precision: 6 }).default( + sql`timezone('utc'::text, now())`, + ), date1: date('date1').default('2024-01-01'), date2: date('date2').defaultNow(), + date3: date('date3').default(sql`CURRENT_TIMESTAMP`), uuid1: uuid('uuid1').default('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'), uuid2: uuid('uuid2').defaultRandom(), inet: inet('inet').default('127.0.0.1'), @@ -418,6 +422,29 @@ test('introspect enum with similar name to native type', async () => { expect(sqlStatements.length).toBe(0); }); +test('instrospect strings with single quotes', async () => { + const client = new PGlite(); + + const myEnum = pgEnum('my_enum', ['escape\'s quotes " ']); + const schema = { + enum_: myEnum, + columns: pgTable('columns', { + enum: myEnum('my_enum').default('escape\'s quotes " '), + text: text('text').default('escape\'s quotes " '), + varchar: varchar('varchar').default('escape\'s quotes " '), + }), + }; + + const { statements, sqlStatements } = await introspectPgToFile( + client, + schema, + 'introspect-strings-with-single-quotes', + ); + + expect(statements.length).toBe(0); + expect(sqlStatements.length).toBe(0); +}); + test('introspect checks', async () => { const client = new PGlite(); diff --git a/drizzle-kit/tests/introspect/sqlite.test.ts b/drizzle-kit/tests/introspect/sqlite.test.ts index 89cdf590e..de13d4e81 100644 --- a/drizzle-kit/tests/introspect/sqlite.test.ts +++ b/drizzle-kit/tests/introspect/sqlite.test.ts @@ -56,6 +56,25 @@ test('generated always column virtual: link to another column', async () => { expect(sqlStatements.length).toBe(0); }); +test('instrospect strings with single quotes', async () => { + const sqlite = new Database(':memory:'); + + const schema = { + columns: sqliteTable('columns', { + text: text('text').default('escape\'s quotes " '), + }), + }; + + const { statements, sqlStatements } = await introspectSQLiteToFile( + sqlite, + schema, + 'introspect-strings-with-single-quotes', + ); + + 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.test.ts b/drizzle-kit/tests/mysql.test.ts index 183464ec0..881b05ef7 100644 --- a/drizzle-kit/tests/mysql.test.ts +++ b/drizzle-kit/tests/mysql.test.ts @@ -4,6 +4,7 @@ import { index, int, json, + mysqlEnum, mysqlSchema, mysqlTable, primaryKey, @@ -11,6 +12,7 @@ import { text, unique, uniqueIndex, + varchar, } from 'drizzle-orm/mysql-core'; import { expect, test } from 'vitest'; import { diffTestSchemasMysql } from './schemaDiffer'; @@ -533,6 +535,32 @@ test('drop index', async () => { expect(sqlStatements[0]).toBe('DROP INDEX `name_idx` ON `table`;'); }); +test('drop unique constraint', async () => { + const from = { + users: mysqlTable( + 'table', + { + name: text('name'), + }, + (t) => { + return { + uq: unique('name_uq').on(t.name), + }; + }, + ), + }; + + const to = { + users: mysqlTable('table', { + name: text('name'), + }), + }; + + const { sqlStatements } = await diffTestSchemasMysql(from, to, []); + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toBe('ALTER TABLE `table` DROP INDEX `name_uq`;'); +}); + test('add table with indexes', async () => { const from = {}; @@ -578,6 +606,80 @@ test('add table with indexes', async () => { ]); }); +test('varchar and text default values escape single quotes', async (t) => { + const schema1 = { + table: mysqlTable('table', { + id: serial('id').primaryKey(), + }), + }; + + const schem2 = { + table: mysqlTable('table', { + id: serial('id').primaryKey(), + 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"), + }), + }; + + const { sqlStatements } = await diffTestSchemasMysql(schema1, schem2, []); + + expect(sqlStatements.length).toBe(3); + expect(sqlStatements[0]).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';", + ); +}); + +test('composite primary key', async () => { + const from = {}; + const to = { + table: mysqlTable('works_to_creators', { + workId: int('work_id').notNull(), + creatorId: int('creator_id').notNull(), + classification: text('classification').notNull(), + }, (t) => ({ + pk: primaryKey({ + columns: [t.workId, t.creatorId, t.classification], + }), + })), + }; + + const { sqlStatements } = await diffTestSchemasMysql(from, to, []); + + expect(sqlStatements).toStrictEqual([ + 'CREATE TABLE `works_to_creators` (\n\t`work_id` int NOT NULL,\n\t`creator_id` int NOT NULL,\n\t`classification` text NOT NULL,\n\tCONSTRAINT `works_to_creators_work_id_creator_id_classification_pk` PRIMARY KEY(`work_id`,`creator_id`,`classification`)\n);\n', + ]); +}); + +test('add column before creating unique constraint', async () => { + const from = { + table: mysqlTable('table', { + id: serial('id').primaryKey(), + }), + }; + const to = { + table: mysqlTable('table', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + }, (t) => ({ + uq: unique('uq').on(t.name), + })), + }; + + const { sqlStatements } = await diffTestSchemasMysql(from, to, []); + + expect(sqlStatements).toStrictEqual([ + 'ALTER TABLE `table` ADD `name` text NOT NULL;', + 'ALTER TABLE `table` ADD CONSTRAINT `uq` UNIQUE(`name`);', + ]); +}); + test('optional db aliases (snake case)', async () => { const from = {}; diff --git a/drizzle-kit/tests/pg-columns.test.ts b/drizzle-kit/tests/pg-columns.test.ts index cffeed3ed..ddd744a81 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 } from 'drizzle-orm/pg-core'; +import { integer, pgTable, primaryKey, serial, text, uuid, varchar } from 'drizzle-orm/pg-core'; import { expect, test } from 'vitest'; import { diffTestSchemas } from './schemaDiffer'; @@ -456,3 +456,29 @@ test('add multiple constraints #3', async (t) => { expect(statements.length).toBe(6); }); + +test('varchar and text default values escape single quotes', async (t) => { + const schema1 = { + table: pgTable('table', { + id: serial('id').primaryKey(), + }), + }; + + const schem2 = { + table: pgTable('table', { + id: serial('id').primaryKey(), + text: text('text').default("escape's quotes"), + varchar: varchar('varchar').default("escape's quotes"), + }), + }; + + const { sqlStatements } = await diffTestSchemas(schema1, schem2, []); + + expect(sqlStatements.length).toBe(2); + expect(sqlStatements[0]).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\';', + ); +}); diff --git a/drizzle-kit/tests/pg-enums.test.ts b/drizzle-kit/tests/pg-enums.test.ts index 99a3dca7e..2af691d46 100644 --- a/drizzle-kit/tests/pg-enums.test.ts +++ b/drizzle-kit/tests/pg-enums.test.ts @@ -1,4 +1,4 @@ -import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'; +import { integer, pgEnum, pgSchema, pgTable, serial } from 'drizzle-orm/pg-core'; import { expect, test } from 'vitest'; import { diffTestSchemas } from './schemaDiffer'; @@ -506,6 +506,77 @@ test('enums #18', async () => { }); }); +test('enums #19', async () => { + const myEnum = pgEnum('my_enum', ["escape's quotes"]); + + const from = {}; + + const to = { myEnum }; + + const { sqlStatements } = await diffTestSchemas(from, to, []); + + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toStrictEqual( + 'CREATE TYPE "public"."my_enum" AS ENUM(\'escape\'\'s quotes\');', + ); +}); + +test('enums #20', async () => { + const myEnum = pgEnum('my_enum', ['one', 'two', 'three']); + + const from = { + myEnum, + table: pgTable('table', { + id: serial('id').primaryKey(), + }), + }; + + const to = { + myEnum, + table: pgTable('table', { + id: serial('id').primaryKey(), + col1: myEnum('col1'), + col2: integer('col2'), + }), + }; + + const { sqlStatements } = await diffTestSchemas(from, to, []); + + expect(sqlStatements.length).toBe(2); + expect(sqlStatements).toStrictEqual([ + 'ALTER TABLE "table" ADD COLUMN "col1" "my_enum";', + 'ALTER TABLE "table" ADD COLUMN "col2" integer;', + ]); +}); + +test('enums #21', async () => { + const myEnum = pgEnum('my_enum', ['one', 'two', 'three']); + + const from = { + myEnum, + table: pgTable('table', { + id: serial('id').primaryKey(), + }), + }; + + const to = { + myEnum, + table: pgTable('table', { + id: serial('id').primaryKey(), + col1: myEnum('col1').array(), + col2: integer('col2').array(), + }), + }; + + const { sqlStatements } = await diffTestSchemas(from, to, []); + + expect(sqlStatements.length).toBe(2); + expect(sqlStatements).toStrictEqual([ + 'ALTER TABLE "table" ADD COLUMN "col1" "my_enum"[];', + 'ALTER TABLE "table" ADD COLUMN "col2" integer[];', + ]); +}); + test('drop enum value', async () => { const enum1 = pgEnum('enum', ['value1', 'value2', 'value3']); diff --git a/drizzle-kit/tests/pg-tables.test.ts b/drizzle-kit/tests/pg-tables.test.ts index 1f2885f92..6ea6e472a 100644 --- a/drizzle-kit/tests/pg-tables.test.ts +++ b/drizzle-kit/tests/pg-tables.test.ts @@ -676,6 +676,106 @@ test('create table with tsvector', async () => { ]); }); +test('composite primary key', async () => { + const from = {}; + const to = { + table: pgTable('works_to_creators', { + workId: integer('work_id').notNull(), + creatorId: integer('creator_id').notNull(), + classification: text('classification').notNull(), + }, (t) => ({ + pk: primaryKey({ + columns: [t.workId, t.creatorId, t.classification], + }), + })), + }; + + const { sqlStatements } = await diffTestSchemas(from, to, []); + + expect(sqlStatements).toStrictEqual([ + 'CREATE TABLE IF NOT EXISTS "works_to_creators" (\n\t"work_id" integer NOT NULL,\n\t"creator_id" integer NOT NULL,\n\t"classification" text NOT NULL,\n\tCONSTRAINT "works_to_creators_work_id_creator_id_classification_pk" PRIMARY KEY("work_id","creator_id","classification")\n);\n', + ]); +}); + +test('add column before creating unique constraint', async () => { + const from = { + table: pgTable('table', { + id: serial('id').primaryKey(), + }), + }; + const to = { + table: pgTable('table', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + }, (t) => ({ + uq: unique('uq').on(t.name), + })), + }; + + const { sqlStatements } = await diffTestSchemas(from, to, []); + + expect(sqlStatements).toStrictEqual([ + 'ALTER TABLE "table" ADD COLUMN "name" text NOT NULL;', + 'ALTER TABLE "table" ADD CONSTRAINT "uq" UNIQUE("name");', + ]); +}); + +test('alter composite primary key', async () => { + const from = { + table: pgTable('table', { + col1: integer('col1').notNull(), + col2: integer('col2').notNull(), + col3: text('col3').notNull(), + }, (t) => ({ + pk: primaryKey({ + name: 'table_pk', + columns: [t.col1, t.col2], + }), + })), + }; + const to = { + table: pgTable('table', { + col1: integer('col1').notNull(), + col2: integer('col2').notNull(), + col3: text('col3').notNull(), + }, (t) => ({ + pk: primaryKey({ + name: 'table_pk', + columns: [t.col2, t.col3], + }), + })), + }; + + const { sqlStatements } = await diffTestSchemas(from, to, []); + + expect(sqlStatements).toStrictEqual([ + 'ALTER TABLE "table" DROP CONSTRAINT "table_pk";\n--> statement-breakpoint\nALTER TABLE "table" ADD CONSTRAINT "table_pk" PRIMARY KEY("col2","col3");', + ]); +}); + +test('add index with op', async () => { + const from = { + users: pgTable('users', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + }), + }; + const to = { + users: pgTable('users', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + }, (t) => ({ + nameIdx: index().using('gin', t.name.op('gin_trgm_ops')), + })), + }; + + const { sqlStatements } = await diffTestSchemas(from, to, []); + + expect(sqlStatements).toStrictEqual([ + 'CREATE INDEX IF NOT EXISTS "users_name_index" ON "users" USING gin ("name" gin_trgm_ops);', + ]); +}); + test('optional db aliases (snake case)', async () => { const from = {}; diff --git a/drizzle-kit/tests/push/common.ts b/drizzle-kit/tests/push/common.ts index e5c68625d..627070f11 100644 --- a/drizzle-kit/tests/push/common.ts +++ b/drizzle-kit/tests/push/common.ts @@ -15,6 +15,8 @@ export interface DialectSuite { dropGeneratedConstraint(context?: any): Promise; alterGeneratedConstraint(context?: any): Promise; createTableWithGeneratedConstraint(context?: any): Promise; + createCompositePrimaryKey(context?: any): Promise; + renameTableWithCompositePrimaryKey(context?: any): Promise; case1(): Promise; } @@ -48,6 +50,9 @@ export const run = ( // should ignore on push test('Alter generated constraint', () => suite.alterGeneratedConstraint(context)); test('Create table with generated column', () => suite.createTableWithGeneratedConstraint(context)); + test('Rename table with composite primary key', () => suite.renameTableWithCompositePrimaryKey(context)); + + test('Create composite primary key', () => suite.createCompositePrimaryKey(context)); afterAll(afterAllFn ? () => afterAllFn(context) : () => {}); }; diff --git a/drizzle-kit/tests/push/mysql.test.ts b/drizzle-kit/tests/push/mysql.test.ts index 7b20dc444..6c7f5efc2 100644 --- a/drizzle-kit/tests/push/mysql.test.ts +++ b/drizzle-kit/tests/push/mysql.test.ts @@ -15,6 +15,7 @@ import { mediumint, mysqlEnum, mysqlTable, + primaryKey, serial, smallint, text, @@ -29,7 +30,7 @@ import getPort from 'get-port'; import { Connection, createConnection } from 'mysql2/promise'; import { diffTestSchemasMysql, diffTestSchemasPushMysql } from 'tests/schemaDiffer'; import { v4 as uuid } from 'uuid'; -import { expect } from 'vitest'; +import { expect, test } from 'vitest'; import { DialectSuite, run } from './common'; async function createDockerDB(context: any): Promise { @@ -663,6 +664,88 @@ const mysqlSuite: DialectSuite = { createTableWithGeneratedConstraint: function(context?: any): Promise { return {} as any; }, + createCompositePrimaryKey: async function(context: any): Promise { + const schema1 = {}; + + const schema2 = { + table: mysqlTable('table', { + col1: int('col1').notNull(), + col2: int('col2').notNull(), + }, (t) => ({ + pk: primaryKey({ + columns: [t.col1, t.col2], + }), + })), + }; + + const { statements, sqlStatements } = await diffTestSchemasPushMysql( + context.client as Connection, + schema1, + schema2, + [], + 'drizzle', + false, + ); + + expect(statements).toStrictEqual([ + { + type: 'create_table', + tableName: 'table', + schema: undefined, + internals: { + indexes: {}, + tables: {}, + }, + compositePKs: ['table_col1_col2_pk;col1,col2'], + compositePkName: 'table_col1_col2_pk', + uniqueConstraints: [], + checkConstraints: [], + columns: [ + { name: 'col1', type: 'int', primaryKey: false, notNull: true, autoincrement: false }, + { name: 'col2', type: 'int', primaryKey: false, notNull: true, autoincrement: false }, + ], + }, + ]); + expect(sqlStatements).toStrictEqual([ + 'CREATE TABLE `table` (\n\t`col1` int NOT NULL,\n\t`col2` int NOT NULL,\n\tCONSTRAINT `table_col1_col2_pk` PRIMARY KEY(`col1`,`col2`)\n);\n', + ]); + }, + renameTableWithCompositePrimaryKey: async function(context?: any): Promise { + const productsCategoriesTable = (tableName: string) => { + return mysqlTable(tableName, { + productId: varchar('product_id', { length: 10 }).notNull(), + categoryId: varchar('category_id', { length: 10 }).notNull(), + }, (t) => ({ + pk: primaryKey({ + columns: [t.productId, t.categoryId], + }), + })); + }; + + const schema1 = { + table: productsCategoriesTable('products_categories'), + }; + const schema2 = { + test: productsCategoriesTable('products_to_categories'), + }; + + const { sqlStatements } = await diffTestSchemasPushMysql( + context.client as Connection, + schema1, + schema2, + ['public.products_categories->public.products_to_categories'], + 'drizzle', + false, + ); + + expect(sqlStatements).toStrictEqual([ + 'RENAME TABLE `products_categories` TO `products_to_categories`;', + 'ALTER TABLE `products_to_categories` DROP PRIMARY KEY;', + 'ALTER TABLE `products_to_categories` ADD PRIMARY KEY(`product_id`,`category_id`);', + ]); + + await context.client.query(`DROP TABLE \`products_categories\``); + }, }; run( diff --git a/drizzle-kit/tests/push/pg.test.ts b/drizzle-kit/tests/push/pg.test.ts index 67743d2ef..44ec786b6 100644 --- a/drizzle-kit/tests/push/pg.test.ts +++ b/drizzle-kit/tests/push/pg.test.ts @@ -22,6 +22,7 @@ import { pgSequence, pgTable, pgView, + primaryKey, real, serial, smallint, @@ -914,6 +915,89 @@ const pgSuite: DialectSuite = { expect(shouldAskForApprove).toBeFalsy(); }, + async createCompositePrimaryKey() { + const client = new PGlite(); + + const schema1 = {}; + + const schema2 = { + table: pgTable('table', { + col1: integer('col1').notNull(), + col2: integer('col2').notNull(), + }, (t) => ({ + pk: primaryKey({ + columns: [t.col1, t.col2], + }), + })), + }; + + const { statements, sqlStatements } = await diffTestSchemasPush( + client, + schema1, + schema2, + [], + false, + ['public'], + ); + + expect(statements).toStrictEqual([ + { + type: 'create_table', + tableName: 'table', + schema: '', + compositePKs: ['col1,col2;table_col1_col2_pk'], + compositePkName: 'table_col1_col2_pk', + isRLSEnabled: false, + policies: [], + uniqueConstraints: [], + checkConstraints: [], + columns: [ + { name: 'col1', type: 'integer', primaryKey: false, notNull: true }, + { name: 'col2', type: 'integer', primaryKey: false, notNull: true }, + ], + }, + ]); + expect(sqlStatements).toStrictEqual([ + 'CREATE TABLE IF NOT EXISTS "table" (\n\t"col1" integer NOT NULL,\n\t"col2" integer NOT NULL,\n\tCONSTRAINT "table_col1_col2_pk" PRIMARY KEY("col1","col2")\n);\n', + ]); + }, + + async renameTableWithCompositePrimaryKey() { + const client = new PGlite(); + + const productsCategoriesTable = (tableName: string) => { + return pgTable(tableName, { + productId: text('product_id').notNull(), + categoryId: text('category_id').notNull(), + }, (t) => ({ + pk: primaryKey({ + columns: [t.productId, t.categoryId], + }), + })); + }; + + const schema1 = { + table: productsCategoriesTable('products_categories'), + }; + const schema2 = { + test: productsCategoriesTable('products_to_categories'), + }; + + const { sqlStatements } = await diffTestSchemasPush( + client, + schema1, + schema2, + ['public.products_categories->public.products_to_categories'], + false, + ['public'], + ); + expect(sqlStatements).toStrictEqual([ + 'ALTER TABLE "products_categories" RENAME TO "products_to_categories";', + 'ALTER TABLE "products_to_categories" DROP CONSTRAINT "products_categories_product_id_category_id_pk";', + 'ALTER TABLE "products_to_categories" ADD CONSTRAINT "products_to_categories_product_id_category_id_pk" PRIMARY KEY("product_id","category_id");', + ]); + }, + // async addVectorIndexes() { // const client = new PGlite(); @@ -2104,6 +2188,81 @@ test('drop check constraint', async () => { ]); }); +test('Column with same name as enum', async () => { + const client = new PGlite(); + const statusEnum = pgEnum('status', ['inactive', 'active', 'banned']); + + const schema1 = { + statusEnum, + table1: pgTable('table1', { + id: serial('id').primaryKey(), + }), + }; + + const schema2 = { + statusEnum, + table1: pgTable('table1', { + id: serial('id').primaryKey(), + status: statusEnum('status').default('inactive'), + }), + table2: pgTable('table2', { + id: serial('id').primaryKey(), + status: statusEnum('status').default('inactive'), + }), + }; + + const { statements, sqlStatements } = await diffTestSchemasPush( + client, + schema1, + schema2, + [], + false, + ['public'], + ); + + expect(statements).toStrictEqual([ + { + type: 'create_table', + tableName: 'table2', + schema: '', + compositePKs: [], + compositePkName: '', + isRLSEnabled: false, + policies: [], + uniqueConstraints: [], + checkConstraints: [], + columns: [ + { name: 'id', type: 'serial', primaryKey: true, notNull: true }, + { + name: 'status', + type: 'status', + typeSchema: 'public', + primaryKey: false, + notNull: false, + default: "'inactive'", + }, + ], + }, + { + type: 'alter_table_add_column', + tableName: 'table1', + schema: '', + column: { + name: 'status', + type: 'status', + typeSchema: 'public', + primaryKey: false, + notNull: false, + default: "'inactive'", + }, + }, + ]); + expect(sqlStatements).toStrictEqual([ + 'CREATE TABLE IF NOT EXISTS "table2" (\n\t"id" serial PRIMARY KEY NOT NULL,\n\t"status" "status" DEFAULT \'inactive\'\n);\n', + 'ALTER TABLE "table1" ADD COLUMN "status" "status" DEFAULT \'inactive\';', + ]); +}); + test('db has checks. Push with same names', async () => { const client = new PGlite(); @@ -2755,9 +2914,7 @@ test('add policy', async () => { as: 'PERMISSIVE', for: 'ALL', to: ['public'], - using: undefined, on: undefined, - withCheck: undefined, }, schema: '', }, @@ -2814,8 +2971,6 @@ test('drop policy', async () => { for: 'ALL', to: ['public'], on: undefined, - using: undefined, - withCheck: undefined, }, schema: '', }, @@ -2868,9 +3023,7 @@ test('add policy without enable rls', async () => { as: 'PERMISSIVE', for: 'ALL', to: ['public'], - using: undefined, on: undefined, - withCheck: undefined, }, schema: '', }, @@ -2922,9 +3075,7 @@ test('drop policy without disable rls', async () => { as: 'PERMISSIVE', for: 'ALL', to: ['public'], - using: undefined, on: undefined, - withCheck: undefined, }, schema: '', }, @@ -3098,8 +3249,6 @@ test('alter policy with recreation: changing as', async (t) => { name: 'test', to: ['public'], on: undefined, - using: undefined, - withCheck: undefined, }, schema: '', tableName: 'users', @@ -3112,8 +3261,6 @@ test('alter policy with recreation: changing as', async (t) => { name: 'test', to: ['public'], on: undefined, - using: undefined, - withCheck: undefined, }, schema: '', tableName: 'users', @@ -3166,8 +3313,6 @@ test('alter policy with recreation: changing for', async (t) => { name: 'test', to: ['public'], on: undefined, - using: undefined, - withCheck: undefined, }, schema: '', tableName: 'users', @@ -3179,9 +3324,7 @@ test('alter policy with recreation: changing for', async (t) => { for: 'DELETE', name: 'test', to: ['public'], - using: undefined, on: undefined, - withCheck: undefined, }, schema: '', tableName: 'users', @@ -3233,9 +3376,7 @@ test('alter policy with recreation: changing both "as" and "for"', async (t) => for: 'ALL', name: 'test', to: ['public'], - using: undefined, on: undefined, - withCheck: undefined, }, schema: '', tableName: 'users', @@ -3247,9 +3388,7 @@ test('alter policy with recreation: changing both "as" and "for"', async (t) => for: 'INSERT', name: 'test', to: ['public'], - using: undefined, on: undefined, - withCheck: undefined, }, schema: '', tableName: 'users', @@ -3301,9 +3440,7 @@ test('alter policy with recreation: changing all fields', async (t) => { for: 'SELECT', name: 'test', to: ['public'], - using: undefined, on: undefined, - withCheck: undefined, }, schema: '', tableName: 'users', @@ -3316,8 +3453,6 @@ test('alter policy with recreation: changing all fields', async (t) => { name: 'test', to: ['current_role'], on: undefined, - using: undefined, - withCheck: undefined, }, schema: '', tableName: 'users', @@ -3490,9 +3625,7 @@ test('create table with a policy', async (t) => { to: [ 'public', ], - using: undefined, on: undefined, - withCheck: undefined, }, schema: '', tableName: 'users2', @@ -3595,8 +3728,6 @@ test('add policy with multiple "to" roles', async (t) => { name: 'test', on: undefined, to: ['current_role', 'manager'], - using: undefined, - withCheck: undefined, }, schema: '', tableName: 'users', @@ -3609,6 +3740,223 @@ test('add policy with multiple "to" roles', async (t) => { } }); +test('rename policy that is linked', async (t) => { + const client = new PGlite(); + + const users = pgTable('users', { + id: integer('id').primaryKey(), + }); + + const { sqlStatements: createUsers } = await diffTestSchemas({}, { users }, []); + + const schema1 = { + rls: pgPolicy('test', { as: 'permissive' }).link(users), + }; + + const schema2 = { + users, + rls: pgPolicy('newName', { as: 'permissive' }).link(users), + }; + + const { statements, sqlStatements } = await diffTestSchemasPush( + client, + schema1, + schema2, + ['public.users.test->public.users.newName'], + false, + ['public'], + undefined, + undefined, + { before: createUsers }, + ); + + expect(sqlStatements).toStrictEqual([ + 'ALTER POLICY "test" ON "users" RENAME TO "newName";', + ]); + expect(statements).toStrictEqual([ + { + newName: 'newName', + oldName: 'test', + schema: '', + tableName: 'users', + type: 'rename_policy', + }, + ]); +}); + +test('alter policy that is linked', async (t) => { + const client = new PGlite(); + const users = pgTable('users', { + id: integer('id').primaryKey(), + }); + + const { sqlStatements: createUsers } = await diffTestSchemas({}, { users }, []); + + const schema1 = { + rls: pgPolicy('test', { as: 'permissive' }).link(users), + }; + + const schema2 = { + users, + rls: pgPolicy('test', { as: 'permissive', to: 'current_role' }).link(users), + }; + const { statements, sqlStatements } = await diffTestSchemasPush( + client, + schema1, + schema2, + [], + false, + ['public'], + undefined, + undefined, + { before: createUsers }, + ); + + expect(sqlStatements).toStrictEqual([ + 'ALTER POLICY "test" ON "users" TO current_role;', + ]); + expect(statements).toStrictEqual([{ + newData: 'test--PERMISSIVE--ALL--current_role--undefined', + oldData: 'test--PERMISSIVE--ALL--public--undefined', + schema: '', + tableName: 'users', + type: 'alter_policy', + }]); +}); + +test('alter policy that is linked: withCheck', async (t) => { + const client = new PGlite(); + + const users = pgTable('users', { + id: integer('id').primaryKey(), + }); + + const { sqlStatements: createUsers } = await diffTestSchemas({}, { users }, []); + + const schema1 = { + rls: pgPolicy('test', { as: 'permissive', withCheck: sql`true` }).link(users), + }; + + const schema2 = { + users, + rls: pgPolicy('test', { as: 'permissive', withCheck: sql`false` }).link(users), + }; + + const { statements, sqlStatements } = await diffTestSchemasPush( + client, + schema1, + schema2, + [], + false, + ['public'], + undefined, + undefined, + { before: createUsers }, + ); + + expect(sqlStatements).toStrictEqual([]); + expect(statements).toStrictEqual([]); +}); + +test('alter policy that is linked: using', async (t) => { + const client = new PGlite(); + const users = pgTable('users', { + id: integer('id').primaryKey(), + }); + + const { sqlStatements: createUsers } = await diffTestSchemas({}, { users }, []); + + const schema1 = { + rls: pgPolicy('test', { as: 'permissive', using: sql`true` }).link(users), + }; + + const schema2 = { + users, + rls: pgPolicy('test', { as: 'permissive', using: sql`false` }).link(users), + }; + + const { statements, sqlStatements } = await diffTestSchemasPush( + client, + schema1, + schema2, + [], + false, + ['public'], + undefined, + undefined, + { before: createUsers }, + ); + + expect(sqlStatements).toStrictEqual([]); + expect(statements).toStrictEqual([]); +}); + +test('alter policy that is linked: using', async (t) => { + const client = new PGlite(); + + const users = pgTable('users', { + id: integer('id').primaryKey(), + }); + + const { sqlStatements: createUsers } = await diffTestSchemas({}, { users }, []); + + const schema1 = { + rls: pgPolicy('test', { for: 'insert' }).link(users), + }; + + const schema2 = { + users, + rls: pgPolicy('test', { for: 'delete' }).link(users), + }; + + const { statements, sqlStatements } = await diffTestSchemasPush( + client, + schema1, + schema2, + [], + false, + ['public'], + undefined, + undefined, + { before: createUsers }, + ); + + expect(sqlStatements).toStrictEqual([ + 'DROP POLICY "test" ON "users" CASCADE;', + 'CREATE POLICY "test" ON "users" AS PERMISSIVE FOR DELETE TO public;', + ]); + expect(statements).toStrictEqual([ + { + data: { + as: 'PERMISSIVE', + for: 'INSERT', + name: 'test', + on: undefined, + to: [ + 'public', + ], + }, + schema: '', + tableName: 'users', + type: 'drop_policy', + }, + { + data: { + as: 'PERMISSIVE', + for: 'DELETE', + name: 'test', + on: undefined, + to: [ + 'public', + ], + }, + schema: '', + tableName: 'users', + type: 'create_policy', + }, + ]); +}); + //// test('create role', async (t) => { diff --git a/drizzle-kit/tests/sqlite-columns.test.ts b/drizzle-kit/tests/sqlite-columns.test.ts index b7b4c7f6b..0cb34c220 100644 --- a/drizzle-kit/tests/sqlite-columns.test.ts +++ b/drizzle-kit/tests/sqlite-columns.test.ts @@ -1025,3 +1025,25 @@ test('recreate table with nested references', async (t) => { expect(sqlStatements[4]).toBe(`ALTER TABLE \`__new_users\` RENAME TO \`users\`;`); expect(sqlStatements[5]).toBe(`PRAGMA foreign_keys=ON;`); }); + +test('text default values escape single quotes', async (t) => { + const schema1 = { + table: sqliteTable('table', { + id: integer('id').primaryKey(), + }), + }; + + const schem2 = { + table: sqliteTable('table', { + id: integer('id').primaryKey(), + text: text('text').default("escape's quotes"), + }), + }; + + const { sqlStatements } = await diffTestSchemasSqlite(schema1, schem2, []); + + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toStrictEqual( + "ALTER TABLE `table` ADD `text` text DEFAULT 'escape''s quotes';", + ); +}); diff --git a/drizzle-kit/tests/sqlite-tables.test.ts b/drizzle-kit/tests/sqlite-tables.test.ts index 8d8eae298..651c3633c 100644 --- a/drizzle-kit/tests/sqlite-tables.test.ts +++ b/drizzle-kit/tests/sqlite-tables.test.ts @@ -418,6 +418,50 @@ test('add table with indexes', async () => { ]); }); +test('composite primary key', async () => { + const from = {}; + const to = { + table: sqliteTable('works_to_creators', { + workId: int('work_id').notNull(), + creatorId: int('creator_id').notNull(), + classification: text('classification').notNull(), + }, (t) => ({ + pk: primaryKey({ + columns: [t.workId, t.creatorId, t.classification], + }), + })), + }; + + const { sqlStatements } = await diffTestSchemasSqlite(from, to, []); + + expect(sqlStatements).toStrictEqual([ + 'CREATE TABLE `works_to_creators` (\n\t`work_id` integer NOT NULL,\n\t`creator_id` integer NOT NULL,\n\t`classification` text NOT NULL,\n\tPRIMARY KEY(`work_id`, `creator_id`, `classification`)\n);\n', + ]); +}); + +test('add column before creating unique constraint', async () => { + const from = { + table: sqliteTable('table', { + id: int('id').primaryKey(), + }), + }; + const to = { + table: sqliteTable('table', { + id: int('id').primaryKey(), + name: text('name').notNull(), + }, (t) => ({ + uq: unique('uq').on(t.name), + })), + }; + + const { sqlStatements } = await diffTestSchemasSqlite(from, to, []); + + expect(sqlStatements).toStrictEqual([ + 'ALTER TABLE `table` ADD `name` text NOT NULL;', + 'CREATE UNIQUE INDEX `uq` ON `table` (`name`);', + ]); +}); + test('optional db aliases (snake case)', async () => { const from = {};