diff --git a/drizzle-kit/src/cli/commands/migrate.ts b/drizzle-kit/src/cli/commands/migrate.ts index 3d89d3f285..c1edec60b7 100644 --- a/drizzle-kit/src/cli/commands/migrate.ts +++ b/drizzle-kit/src/cli/commands/migrate.ts @@ -1,5 +1,6 @@ import fs from 'fs'; import { + prepareGoogleSqlMigrationSnapshot, prepareMySqlDbPushSnapshot, prepareMySqlMigrationSnapshot, preparePgDbPushSnapshot, @@ -20,6 +21,7 @@ import { MySqlSchema, mysqlSchema, squashMysqlScheme, ViewSquashed } from '../.. import { PgSchema, pgSchema, Policy, Role, squashPgScheme, View } from '../../serializer/pgSchema'; import { SQLiteSchema, sqliteSchema, squashSqliteScheme, View as SQLiteView } from '../../serializer/sqliteSchema'; import { + applyGooglesqlSnapshotsDiff, applyLibSQLSnapshotsDiff, applyMysqlSnapshotsDiff, applyPgSnapshotsDiff, @@ -55,6 +57,7 @@ import { schema, } from '../views'; import { ExportConfig, GenerateConfig } from './utils'; +import { googlesqlSchema, squashGooglesqlScheme } from 'src/serializer/googlesqlSchema'; export type Named = { name: string; @@ -608,6 +611,67 @@ export const prepareAndMigrateMysql = async (config: GenerateConfig) => { } }; +export const prepareAndMigrateGooglesql = async (config: GenerateConfig) => { + const outFolder = config.out; + const schemaPath = config.schema; + const casing = config.casing; + + try { + // TODO: remove + assertV1OutFolder(outFolder); // TODO: SPANNER - what to do with this? + + const { snapshots, journal } = prepareMigrationFolder(outFolder, 'googlesql'); + const { prev, cur, custom } = await prepareGoogleSqlMigrationSnapshot( + snapshots, + schemaPath, + casing, + ); + + const validatedPrev = googlesqlSchema.parse(prev); + const validatedCur = googlesqlSchema.parse(cur); + + if (config.custom) { + writeResult({ + cur: custom, + sqlStatements: [], + journal, + outFolder, + name: config.name, + breakpoints: config.breakpoints, + type: 'custom', + prefixMode: config.prefix, + }); + return; + } + + const squashedPrev = squashGooglesqlScheme(validatedPrev); + const squashedCur = squashGooglesqlScheme(validatedCur); + + const { sqlStatements, statements, _meta } = await applyGooglesqlSnapshotsDiff( + squashedPrev, + squashedCur, + tablesResolver, + columnsResolver, + mySqlViewsResolver, + validatedPrev, + validatedCur, + ); + + writeResult({ + cur, + sqlStatements, + journal, + _meta, + outFolder, + name: config.name, + breakpoints: config.breakpoints, + prefixMode: config.prefix, + }); + } catch (e) { + console.error(e); + } +}; + // Not needed for now function singleStoreSchemaSuggestions( curSchema: TypeOf, diff --git a/drizzle-kit/src/cli/commands/utils.ts b/drizzle-kit/src/cli/commands/utils.ts index 6c18b6901e..e9072ba20a 100644 --- a/drizzle-kit/src/cli/commands/utils.ts +++ b/drizzle-kit/src/cli/commands/utils.ts @@ -46,6 +46,12 @@ import { SqliteCredentials, sqliteCredentials, } from '../validations/sqlite'; +import { + printConfigConnectionIssues as printIssuesSpanner, + // SpannerCredentials, + spannerCredentials, +} from '../validations/spanner'; + import { studioCliParams, studioConfig } from '../validations/studio'; import { error, grey } from '../views'; @@ -444,8 +450,15 @@ export const preparePushConfig = async ( ), ); process.exit(1); - } else if (config.dialect === 'googlesql') { - throw new Error('Not implemented'); // TODO: SPANNER + } + + if (config.dialect === 'googlesql') { + console.log( + error( + `You can't use 'push' command with googlesql dialect`, // TODO: SPANNER - not a priority + ), + ); + process.exit(1); } assertUnreachable(config.dialect); @@ -645,8 +658,15 @@ export const preparePullConfig = async ( prefix: config.migrations?.prefix || 'index', entities: config.entities, }; - } else if (dialect === 'googlesql') { - throw new Error('Not implemented'); // TODO: SPANNER + } + + if (dialect === 'googlesql') { + console.log( + error( + `You can't use 'pull' command with googlesql dialect`, // TODO: SPANNER - not a priority + ), + ); + process.exit(1); } assertUnreachable(dialect); @@ -758,15 +778,15 @@ export const prepareStudioConfig = async (options: Record) => { ), ); process.exit(1); - } else if (dialect === 'googlesql') { - throw new Error('Not implemented'); // TODO: SPANNER - not a priority - return { - dialect, - schema, - host, - port, - credentials: null as any, - }; + } + + if (dialect === 'googlesql') { + console.log( + error( + `You can't use 'studio' command with googlesql dialect`, // TODO: SPANNER - not a priority + ), + ); + process.exit(1); } assertUnreachable(dialect); @@ -880,7 +900,17 @@ export const prepareMigrateConfig = async (configPath: string | undefined) => { } if (dialect === 'googlesql') { - throw new Error('Not implemented'); // TODO: SPANNER + console.log( + error( + `You can't use 'migrate' command with googlesql dialect (YET)`, + ), + ); + process.exit(1); + const parsed = spannerCredentials.safeParse(config); + if (!parsed.success) { + printIssuesSpanner(config); + process.exit(1); + } return { dialect, out, diff --git a/drizzle-kit/src/cli/schema.ts b/drizzle-kit/src/cli/schema.ts index 0b15379bfa..f4d534d30e 100644 --- a/drizzle-kit/src/cli/schema.ts +++ b/drizzle-kit/src/cli/schema.ts @@ -29,6 +29,7 @@ import { assertOrmCoreVersion, assertPackages, assertStudioNodeVersion, ormVersi import { assertCollisions, drivers, prefixes } from './validations/common'; import { withStyle } from './validations/outputs'; import { error, grey, MigrateProgress } from './views'; +import { prepareAndMigrateGooglesql } from './commands/migrate'; const optionDialect = string('dialect') .enum(...dialects) @@ -107,7 +108,7 @@ export const generate = command({ ); process.exit(1); } else if (dialect === 'googlesql') { - throw new Error('Not implemented'); // TODO: SPANNER + await prepareAndMigrateGooglesql(opts); } else { assertUnreachable(dialect); } @@ -211,7 +212,12 @@ export const migrate = command({ ); process.exit(1); } else if (dialect === 'googlesql') { - throw new Error('Not implemented'); // TODO: SPANNER + console.log( + error( + `You can't use 'migrate' command with googlesql dialect (yet)`, // TODO: SPANNER - high priority + ), + ); + process.exit(1); } else { assertUnreachable(dialect); } diff --git a/drizzle-kit/src/cli/validations/googlesql.ts b/drizzle-kit/src/cli/validations/googlesql.ts deleted file mode 100644 index a34c22264d..0000000000 --- a/drizzle-kit/src/cli/validations/googlesql.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { boolean, coerce, object, string, TypeOf, union } from 'zod'; -import { error } from '../views'; -import { wrapParam } from './common'; -import { outputs } from './outputs'; - -// TODO: SPANNER - add proper credentials config - -export const googlesqlCredentials = union([ - object({ - host: string().min(1), - port: coerce.number().min(1).optional(), - user: string().min(1).optional(), - // password: string().min(1).optional(), - // database: string().min(1), - // ssl: union([ - // string(), - // object({ - // pfx: string().optional(), - // key: string().optional(), - // passphrase: string().optional(), - // cert: string().optional(), - // ca: union([string(), string().array()]).optional(), - // crl: union([string(), string().array()]).optional(), - // ciphers: string().optional(), - // rejectUnauthorized: boolean().optional(), - // }), - // ]).optional(), - }), - object({ - url: string().min(1), - }), -]); - -export type GoogleSqlCredentials = TypeOf; - -// TODO: SPANNER - add proper connection issues -export const printCliConnectionIssues = (options: any) => { - // const { uri, host, database } = options || {}; - - // if (!uri && (!host || !database)) { - // console.log(outputs.googlesql.connection.required()); - // } -}; - -// TODO: SPANNER - add proper connection issues -export const printConfigConnectionIssues = ( - options: Record, -) => { - // if ('url' in options) { - // let text = `Please provide required params for MySQL driver:\n`; - // console.log(error(text)); - // console.log(wrapParam('url', options.url, false, 'url')); - // process.exit(1); - // } - - // let text = `Please provide required params for MySQL driver:\n`; - // console.log(error(text)); - // console.log(wrapParam('host', options.host)); - // console.log(wrapParam('port', options.port, true)); - // console.log(wrapParam('user', options.user, true)); - // console.log(wrapParam('password', options.password, true, 'secret')); - // console.log(wrapParam('database', options.database)); - // console.log(wrapParam('ssl', options.ssl, true)); - // process.exit(1); -}; diff --git a/drizzle-kit/src/cli/validations/outputs.ts b/drizzle-kit/src/cli/validations/outputs.ts index d93197643a..684bdea4c0 100644 --- a/drizzle-kit/src/cli/validations/outputs.ts +++ b/drizzle-kit/src/cli/validations/outputs.ts @@ -14,7 +14,7 @@ export const outputs = { studio: { drivers: (param: string) => withStyle.error( - `"${param}" is not a valid driver. Available drivers: "pg", "mysql2", "better-sqlite", "libsql", "turso". You can read more about drizzle.config: https://orm.drizzle.team/kit-docs/config-reference`, + `"${param}" is not a valid driver. Available drivers: "pg", "mysql2", "better-sqlite", "libsql", "turso", "spanner". You can read more about drizzle.config: https://orm.drizzle.team/kit-docs/config-reference`, ), noCredentials: () => withStyle.error( @@ -91,10 +91,10 @@ export const outputs = { googlesql: { connection: { driver: () => withStyle.error(`Only "spanner" is available options for "--driver"`), - required: () => - withStyle.error( - `Either "url" or "host", "database" are required for database connection`, // TODO: SPANNER - write proper error message - ), + // required: () => + // withStyle.error( + // `Either "url" or "host", "database" are required for database connection`, // TODO: SPANNER - write proper error message + // ), }, }, }; diff --git a/drizzle-kit/src/cli/validations/spanner.ts b/drizzle-kit/src/cli/validations/spanner.ts new file mode 100644 index 0000000000..c3228565ba --- /dev/null +++ b/drizzle-kit/src/cli/validations/spanner.ts @@ -0,0 +1,35 @@ +import { boolean, coerce, object, string, TypeOf, union } from 'zod'; +import { error } from '../views'; +import { wrapParam } from './common'; +import { outputs } from './outputs'; + +// TODO: SPANNER - add proper credentials config + +export const spannerCredentials = object({ + projectId: string().min(1), + instanceId: string().min(1), + databaseId: string().min(1), +}); + +export type SpannerCredentials = TypeOf; + +// TODO: SPANNER - add proper connection issues +// export const printCliConnectionIssues = (options: any) => { + // const { uri, host, database } = options || {}; + + // if (!uri && (!host || !database)) { + // console.log(outputs.googlesql.connection.required()); + // } +// }; + +// TODO: SPANNER - add proper connection issues +export const printConfigConnectionIssues = ( + options: Record, +) => { + let text = `Please provide required params for Spanner driver:\n`; + console.log(error(text)); + console.log(wrapParam('projectId', options.projectId)); + console.log(wrapParam('instanceId', options.instanceId)); + console.log(wrapParam('databaseId', options.databaseId)); + process.exit(1); +}; diff --git a/drizzle-kit/src/jsonStatements.ts b/drizzle-kit/src/jsonStatements.ts index 53e40ca634..5d8df2c561 100644 --- a/drizzle-kit/src/jsonStatements.ts +++ b/drizzle-kit/src/jsonStatements.ts @@ -1755,18 +1755,18 @@ export const prepareAlterColumnsGooglesql = ( // const table = json2.tables[tableName]; // const snapshotColumn = table.columns[columnName]; - + // const columnType = snapshotColumn.type; // const columnDefault = snapshotColumn.default; // const columnOnUpdate = 'onUpdate' in snapshotColumn ? snapshotColumn.onUpdate : undefined; // const columnNotNull = table.columns[columnName].notNull; - + // const columnAutoIncrement = 'autoincrement' in snapshotColumn // ? snapshotColumn.autoincrement ?? false // : false; - + // const columnPk = table.columns[columnName].primaryKey; - + // if (column.autoincrement?.type === 'added') { // statements.push({ // type: 'alter_table_alter_column_set_autoincrement', @@ -1781,12 +1781,12 @@ export const prepareAlterColumnsGooglesql = ( // columnPk, // }); // } - + // if (column.autoincrement?.type === 'changed') { // const type = column.autoincrement.new // ? 'alter_table_alter_column_set_autoincrement' // : 'alter_table_alter_column_drop_autoincrement'; - + // statements.push({ // type, // tableName, @@ -1800,7 +1800,7 @@ export const prepareAlterColumnsGooglesql = ( // columnPk, // }); // } - + // if (column.autoincrement?.type === 'deleted') { // statements.push({ // type: 'alter_table_alter_column_drop_autoincrement', diff --git a/drizzle-kit/src/sqlgenerator.ts b/drizzle-kit/src/sqlgenerator.ts index f41dbbdd08..7f662f4562 100644 --- a/drizzle-kit/src/sqlgenerator.ts +++ b/drizzle-kit/src/sqlgenerator.ts @@ -589,11 +589,8 @@ class GoogleSqlCreateTableConvertor extends Convertor { const { tableName, columns, - schema, checkConstraints, compositePKs, - uniqueConstraints, - internals, } = st; let statement = ''; @@ -605,51 +602,21 @@ class GoogleSqlCreateTableConvertor extends Convertor { const notNullStatement = column.notNull ? ' NOT NULL' : ''; const defaultStatement = column.default !== undefined ? ` DEFAULT ${column.default}` : ''; - const onUpdateStatement = column.onUpdate - ? ` ON UPDATE CURRENT_TIMESTAMP` - : ''; - + // TODO: SPANNER - support auto increment const autoincrementStatement = column.autoincrement ? ' AUTO_INCREMENT' : ''; const generatedStatement = column.generated - ? ` GENERATED ALWAYS AS (${column.generated?.as}) ${column.generated?.type.toUpperCase()}` + ? ` AS (${column.generated?.as})${column.generated?.type === 'stored' ? ' STORED' : ''}` : ''; statement += '\t' - + `\`${column.name}\` ${column.type}${autoincrementStatement}${primaryKeyStatement}${generatedStatement}${notNullStatement}${defaultStatement}${onUpdateStatement}`; + + `\`${column.name}\` ${column.type}${notNullStatement}${autoincrementStatement}${defaultStatement}${generatedStatement}${primaryKeyStatement}`; statement += i === columns.length - 1 ? '' : ',\n'; } - if (typeof compositePKs !== 'undefined' && compositePKs.length > 0) { - statement += ',\n'; - const compositePK = GoogleSqlSquasher.unsquashPK(compositePKs[0]); - statement += `\tCONSTRAINT \`${st.compositePkName}\` PRIMARY KEY(\`${compositePK.columns.join(`\`,\``)}\`)`; - } - - if ( - typeof uniqueConstraints !== 'undefined' - && uniqueConstraints.length > 0 - ) { - for (const uniqueConstraint of uniqueConstraints) { - statement += ',\n'; - const unsquashedUnique = GoogleSqlSquasher.unsquashUnique(uniqueConstraint); - - const uniqueString = unsquashedUnique.columns - .map((it) => { - return internals?.indexes - ? internals?.indexes[unsquashedUnique.name]?.columns[it] - ?.isExpression - ? it - : `\`${it}\`` - : `\`${it}\``; - }) - .join(','); - - statement += `\tCONSTRAINT \`${unsquashedUnique.name}\` UNIQUE(${uniqueString})`; - } - } + // TODO - SPANNER: Where the f*** are the foreign keys? if (typeof checkConstraints !== 'undefined' && checkConstraints.length > 0) { for (const checkConstraint of checkConstraints) { @@ -660,8 +627,16 @@ class GoogleSqlCreateTableConvertor extends Convertor { } } - statement += `\n);`; - statement += `\n`; + statement += `\n)`; + + if (typeof compositePKs !== 'undefined' && compositePKs.length > 0) { + // statement += ',\n'; + const compositePK = GoogleSqlSquasher.unsquashPK(compositePKs[0]); + statement += ` PRIMARY KEY(\`${compositePK.columns.join(`\`,\``)}\`)`; + } + + // statement += `\n);`; + statement += `;`; return statement; } } @@ -899,21 +874,19 @@ class MySqlCreateViewConvertor extends Convertor { } } -// TODO: SPANNER - verify class GoogleSqlCreateViewConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'googlesql_create_view' && dialect === 'googlesql'; } convert(st: JsonCreateGoogleSqlViewStatement) { - const { definition, name, algorithm, sqlSecurity, withCheckOption, replace } = st; + const { definition, name, sqlSecurity, replace } = st; let statement = `CREATE `; statement += replace ? `OR REPLACE ` : ''; - statement += algorithm ? `ALGORITHM = ${algorithm}\n` : ''; + statement += `VIEW \`${name}\`\n`; statement += sqlSecurity ? `SQL SECURITY ${sqlSecurity}\n` : ''; - statement += `VIEW \`${name}\` AS (${definition})`; - statement += withCheckOption ? `\nWITH ${withCheckOption} CHECK OPTION` : ''; + statement += `AS (${definition})`; statement += ';'; @@ -1004,22 +977,21 @@ class MySqlAlterViewConvertor extends Convertor { } } +// TODO: SPANNER - as spanner doesn't have ALTER VIEW, this actually does a CREATE OR REPLACE VIEW. Is this OK? class GoogleSqlAlterViewConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'alter_googlesql_view' && dialect === 'googlesql'; } convert(st: JsonAlterGoogleSqlViewStatement) { - const { name, algorithm, definition, sqlSecurity, withCheckOption } = st; + console.log('this is being called'); + const { name, definition, sqlSecurity } = st; - let statement = `ALTER `; - statement += algorithm ? `ALGORITHM = ${algorithm}\n` : ''; + let statement = `CREATE OR REPLACE VIEW \`${name}\`\n`; statement += sqlSecurity ? `SQL SECURITY ${sqlSecurity}\n` : ''; - statement += `VIEW \`${name}\` AS ${definition}`; - statement += withCheckOption ? `\nWITH ${withCheckOption} CHECK OPTION` : ''; + statement += `AS (${definition})`; statement += ';'; - return statement; } } @@ -1050,16 +1022,14 @@ class MySqlRenameViewConvertor extends Convertor { } } -// TODO: SPANNER - verify class GoogleSqlRenameViewConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'rename_view' && dialect === 'googlesql'; } convert(st: JsonRenameViewStatement) { - const { nameFrom: from, nameTo: to } = st; - - return `RENAME TABLE \`${from}\` TO \`${to}\`;`; + throw new Error('Google Cloud Spanner does not support renaming views'); + return ''; } } @@ -1377,17 +1347,13 @@ class MySQLAlterTableAddUniqueConstraintConvertor extends Convertor { } } -// TODO: SPANNER - verify +// TODO: SPANNER - remove this? class GoogleSqlAlterTableAddUniqueConstraintConvertor extends Convertor { can(statement: JsonCreateUniqueConstraint, dialect: Dialect): boolean { return statement.type === 'create_unique_constraint' && dialect === 'googlesql'; } convert(statement: JsonCreateUniqueConstraint): string { - const unsquashed = GoogleSqlSquasher.unsquashUnique(statement.data); - - return `ALTER TABLE \`${statement.tableName}\` ADD CONSTRAINT \`${unsquashed.name}\` UNIQUE(\`${ - unsquashed.columns.join('`,`') - }\`);`; + throw new Error('Google SQL does not support adding unique constraints. Try using a unique index instead.'); } } @@ -1402,19 +1368,16 @@ class MySQLAlterTableDropUniqueConstraintConvertor extends Convertor { } } -// TODO: SPANNER - verify +// TODO: SPANNER - remove? class GoogleSqlAlterTableDropUniqueConstraintConvertor extends Convertor { can(statement: JsonDeleteUniqueConstraint, dialect: Dialect): boolean { return statement.type === 'delete_unique_constraint' && dialect === 'googlesql'; } convert(statement: JsonDeleteUniqueConstraint): string { - const unsquashed = GoogleSqlSquasher.unsquashUnique(statement.data); - - return `ALTER TABLE \`${statement.tableName}\` DROP INDEX \`${unsquashed.name}\`;`; + throw new Error('Google SQL does not support unique constraints. Try using a unique index instead.'); } } - class MySqlAlterTableAddCheckConstraintConvertor extends Convertor { can(statement: JsonCreateCheckConstraint, dialect: Dialect): boolean { return ( @@ -1818,7 +1781,7 @@ class MySqlRenameTableConvertor extends Convertor { } } -// TODO: SPANNER - verify +// TODO: SPANNER - (low priority) Add Synonym support class GoogleSqlRenameTableConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'rename_table' && dialect === 'googlesql'; @@ -1826,7 +1789,7 @@ class GoogleSqlRenameTableConvertor extends Convertor { convert(statement: JsonRenameTableStatement) { const { tableNameFrom, tableNameTo } = statement; - return `RENAME TABLE \`${tableNameFrom}\` TO \`${tableNameTo}\`;`; + return `ALTER TABLE \`${tableNameFrom}\` RENAME TO \`${tableNameTo}\`;`; } } @@ -1872,7 +1835,6 @@ class MySqlAlterTableRenameColumnConvertor extends Convertor { } } -// TODO: SPANNER - verify class GoogleSqlAlterTableRenameColumnConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return ( @@ -1881,8 +1843,10 @@ class GoogleSqlAlterTableRenameColumnConvertor extends Convertor { } convert(statement: JsonRenameColumnStatement) { - const { tableName, oldColumnName, newColumnName } = statement; - return `ALTER TABLE \`${tableName}\` RENAME COLUMN \`${oldColumnName}\` TO \`${newColumnName}\`;`; + throw new Error( + 'Google SQL does not support renaming columns. Consider creating a new column, migrating data, and then dropping the old one.', + ); + return ``; } } @@ -1941,7 +1905,6 @@ class MySqlAlterTableDropColumnConvertor extends Convertor { } } -// TODO: SPANNER - verify class GoogleSqlAlterTableDropColumnConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'alter_table_drop_column' && dialect === 'googlesql'; @@ -2071,7 +2034,6 @@ class MySqlAlterTableAddColumnConvertor extends Convertor { } } -// TODO: SPANNER - verify class GoogleSqlAlterTableAddColumnConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'alter_table_add_column' && dialect === 'googlesql'; @@ -2083,23 +2045,17 @@ class GoogleSqlAlterTableAddColumnConvertor extends Convertor { name, type, notNull, - primaryKey, - autoincrement, - onUpdate, generated, } = column; const defaultStatement = `${column.default !== undefined ? ` DEFAULT ${column.default}` : ''}`; const notNullStatement = `${notNull ? ' NOT NULL' : ''}`; - const primaryKeyStatement = `${primaryKey ? ' PRIMARY KEY' : ''}`; - const autoincrementStatement = `${autoincrement ? ' AUTO_INCREMENT' : ''}`; - const onUpdateStatement = `${onUpdate ? ' ON UPDATE CURRENT_TIMESTAMP' : ''}`; const generatedStatement = generated - ? ` GENERATED ALWAYS AS (${generated?.as}) ${generated?.type.toUpperCase()}` + ? ` AS (${generated?.as}) ${generated?.type === 'stored' ? 'STORED' : ''}` : ''; - return `ALTER TABLE \`${tableName}\` ADD \`${name}\` ${type}${primaryKeyStatement}${autoincrementStatement}${defaultStatement}${generatedStatement}${notNullStatement}${onUpdateStatement};`; + return `ALTER TABLE \`${tableName}\` ADD COLUMN \`${name}\` ${type}${notNullStatement}${defaultStatement}${generatedStatement};`; } } @@ -2550,36 +2506,28 @@ class GoogleSqlAlterTableAlterColumnAlterGeneratedConvertor extends Convertor { columnName, schema, columnNotNull: notNull, - columnDefault, - columnOnUpdate, - columnAutoIncrement, - columnPk, columnGenerated, } = statement; + const type = statement.newDataType; + + if (columnGenerated?.type === 'stored') { + // Updating the expression of a STORED generated column or an indexed non-stored generated column isn't allowed. + // https://cloud.google.com/spanner/docs/generated-column/how-to#modify-generated-column + throw new Error('Google SQL does not support modifying stored generated columns.'); + } + const tableNameWithSchema = schema ? `\`${schema}\`.\`${tableName}\`` : `\`${tableName}\``; - const addColumnStatement = new GoogleSqlAlterTableAddColumnConvertor().convert({ - schema, - tableName, - column: { - name: columnName, - type: statement.newDataType, - notNull, - default: columnDefault, - onUpdate: columnOnUpdate, - autoincrement: columnAutoIncrement, - primaryKey: columnPk, - generated: columnGenerated, - }, - type: 'alter_table_add_column', - }); + const generatedStatement = columnGenerated + ? ` AS (${columnGenerated?.as})` + : ''; + // TODO: SPANNER - is it ok to return string instead of array? return [ - `ALTER TABLE ${tableNameWithSchema} drop column \`${columnName}\`;`, - addColumnStatement, + `ALTER TABLE ${tableNameWithSchema} ALTER COLUMN \`${columnName}\` ${type}${notNull}${generatedStatement};`, ]; } } @@ -2624,7 +2572,7 @@ class MySqlAlterTableAddPk extends Convertor { } } -// TODO: SPANNER - verify +// TODO: SPANNER - can we remove this? class GoogleSqlAlterTableAddPk extends Convertor { can(statement: JsonStatement, dialect: string): boolean { return ( @@ -2633,7 +2581,8 @@ class GoogleSqlAlterTableAddPk extends Convertor { ); } convert(statement: JsonAlterColumnSetPrimaryKeyStatement): string { - return `ALTER TABLE \`${statement.tableName}\` ADD PRIMARY KEY (\`${statement.columnName}\`);`; + throw new Error('Google SQL does not support adding primary keys.'); + return ``; } } @@ -2649,7 +2598,7 @@ class MySqlAlterTableDropPk extends Convertor { } } -// TODO: SPANNER - verify +// TODO: SPANNER - can we remove this? class GoogleSqlAlterTableDropPk extends Convertor { can(statement: JsonStatement, dialect: string): boolean { return ( @@ -2658,7 +2607,8 @@ class GoogleSqlAlterTableDropPk extends Convertor { ); } convert(statement: JsonAlterColumnDropPrimaryKeyStatement): string { - return `ALTER TABLE \`${statement.tableName}\` DROP PRIMARY KEY`; + throw new Error('Google SQL does not support dropping primary keys.'); + return ``; } } @@ -3025,8 +2975,6 @@ type GoogleSqlModifyColumnStatement = | JsonAlterColumnSetGeneratedStatement | JsonAlterColumnDropGeneratedStatement; -// TODO: SPANNER: possibly remove it as spanner doesn't support much alter column -// TODO: SPANNER - verify class GoogleSqlModifyColumn extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return ( @@ -3050,8 +2998,7 @@ class GoogleSqlModifyColumn extends Convertor { let columnType = ``; let columnDefault: any = ''; let columnNotNull = ''; - let columnOnUpdate = ''; - let columnAutoincrement = ''; + // let columnAutoincrement = ''; let primaryKey = statement.columnPk ? ' PRIMARY KEY' : ''; let columnGenerated = ''; @@ -3061,142 +3008,70 @@ class GoogleSqlModifyColumn extends Convertor { ? ` DEFAULT ${statement.columnDefault}` : ''; columnNotNull = statement.columnNotNull ? ` NOT NULL` : ''; - columnOnUpdate = statement.columnOnUpdate - ? ` ON UPDATE CURRENT_TIMESTAMP` - : ''; - columnAutoincrement = statement.columnAutoIncrement - ? ' AUTO_INCREMENT' - : ''; } else if (statement.type === 'alter_table_alter_column_set_notnull') { columnNotNull = ` NOT NULL`; columnType = ` ${statement.newDataType}`; columnDefault = statement.columnDefault ? ` DEFAULT ${statement.columnDefault}` : ''; - columnOnUpdate = statement.columnOnUpdate - ? ` ON UPDATE CURRENT_TIMESTAMP` - : ''; - columnAutoincrement = statement.columnAutoIncrement - ? ' AUTO_INCREMENT' - : ''; } else if (statement.type === 'alter_table_alter_column_drop_on_update') { - columnNotNull = statement.columnNotNull ? ` NOT NULL` : ''; - columnType = ` ${statement.newDataType}`; - columnDefault = statement.columnDefault - ? ` DEFAULT ${statement.columnDefault}` - : ''; - columnOnUpdate = ''; - columnAutoincrement = statement.columnAutoIncrement - ? ' AUTO_INCREMENT' - : ''; + throw new Error('Spanner does not support ON UPDATE'); } else if (statement.type === 'alter_table_alter_column_set_on_update') { - columnNotNull = statement.columnNotNull ? ` NOT NULL` : ''; - columnOnUpdate = ` ON UPDATE CURRENT_TIMESTAMP`; - columnType = ` ${statement.newDataType}`; - columnDefault = statement.columnDefault - ? ` DEFAULT ${statement.columnDefault}` - : ''; - columnAutoincrement = statement.columnAutoIncrement - ? ' AUTO_INCREMENT' - : ''; + throw new Error('Spanner does not support ON UPDATE'); } else if ( statement.type === 'alter_table_alter_column_set_autoincrement' ) { - columnNotNull = statement.columnNotNull ? ` NOT NULL` : ''; - columnOnUpdate = columnOnUpdate = statement.columnOnUpdate - ? ` ON UPDATE CURRENT_TIMESTAMP` - : ''; - columnType = ` ${statement.newDataType}`; - columnDefault = statement.columnDefault - ? ` DEFAULT ${statement.columnDefault}` - : ''; - columnAutoincrement = ' AUTO_INCREMENT'; + throw new Error('Not implemented'); + // columnNotNull = statement.columnNotNull ? ` NOT NULL` : ''; + // columnType = ` ${statement.newDataType}`; + // columnDefault = statement.columnDefault + // ? ` DEFAULT ${statement.columnDefault}` + // : ''; + // columnAutoincrement = ' AUTO_INCREMENT'; } else if ( statement.type === 'alter_table_alter_column_drop_autoincrement' ) { - columnNotNull = statement.columnNotNull ? ` NOT NULL` : ''; - columnOnUpdate = columnOnUpdate = statement.columnOnUpdate - ? ` ON UPDATE CURRENT_TIMESTAMP` - : ''; - columnType = ` ${statement.newDataType}`; - columnDefault = statement.columnDefault - ? ` DEFAULT ${statement.columnDefault}` - : ''; - columnAutoincrement = ''; + throw new Error('Not implemented'); + // columnNotNull = statement.columnNotNull ? ` NOT NULL` : ''; + // columnType = ` ${statement.newDataType}`; + // columnDefault = statement.columnDefault + // ? ` DEFAULT ${statement.columnDefault}` + // : ''; + // columnAutoincrement = ''; } else if (statement.type === 'alter_table_alter_column_set_default') { columnNotNull = statement.columnNotNull ? ` NOT NULL` : ''; - columnOnUpdate = columnOnUpdate = statement.columnOnUpdate - ? ` ON UPDATE CURRENT_TIMESTAMP` - : ''; columnType = ` ${statement.newDataType}`; columnDefault = ` DEFAULT ${statement.newDefaultValue}`; - columnAutoincrement = statement.columnAutoIncrement - ? ' AUTO_INCREMENT' - : ''; + // columnAutoincrement = statement.columnAutoIncrement + // ? ' AUTO_INCREMENT' + // : ''; } else if (statement.type === 'alter_table_alter_column_drop_default') { columnNotNull = statement.columnNotNull ? ` NOT NULL` : ''; - columnOnUpdate = columnOnUpdate = statement.columnOnUpdate - ? ` ON UPDATE CURRENT_TIMESTAMP` - : ''; columnType = ` ${statement.newDataType}`; columnDefault = ''; - columnAutoincrement = statement.columnAutoIncrement - ? ' AUTO_INCREMENT' - : ''; + // columnAutoincrement = statement.columnAutoIncrement + // ? ' AUTO_INCREMENT' + // : ''; } else if (statement.type === 'alter_table_alter_column_set_generated') { columnType = ` ${statement.newDataType}`; columnNotNull = statement.columnNotNull ? ` NOT NULL` : ''; - columnOnUpdate = columnOnUpdate = statement.columnOnUpdate - ? ` ON UPDATE CURRENT_TIMESTAMP` - : ''; columnDefault = statement.columnDefault ? ` DEFAULT ${statement.columnDefault}` : ''; - columnAutoincrement = statement.columnAutoIncrement - ? ' AUTO_INCREMENT' + // TODO: SPANNER - verify if this works for both virtual and stored + columnGenerated = statement.columnGenerated + ? ` AS (${statement.columnGenerated?.as}) ${statement.columnGenerated?.type === 'stored' ? 'STORED' : ''}` : ''; - - if (statement.columnGenerated?.type === 'virtual') { - return [ - new GoogleSqlAlterTableDropColumnConvertor().convert({ - type: 'alter_table_drop_column', - tableName: statement.tableName, - columnName: statement.columnName, - schema: statement.schema, - }), - new GoogleSqlAlterTableAddColumnConvertor().convert({ - tableName, - column: { - name: columnName, - type: statement.newDataType, - notNull: statement.columnNotNull, - default: statement.columnDefault, - onUpdate: statement.columnOnUpdate, - autoincrement: statement.columnAutoIncrement, - primaryKey: statement.columnPk, - generated: statement.columnGenerated, - }, - schema: statement.schema, - type: 'alter_table_add_column', - }), - ]; - } else { - columnGenerated = statement.columnGenerated - ? ` GENERATED ALWAYS AS (${statement.columnGenerated?.as}) ${statement.columnGenerated?.type.toUpperCase()}` - : ''; - } } else if (statement.type === 'alter_table_alter_column_drop_generated') { columnType = ` ${statement.newDataType}`; columnNotNull = statement.columnNotNull ? ` NOT NULL` : ''; - columnOnUpdate = columnOnUpdate = statement.columnOnUpdate - ? ` ON UPDATE CURRENT_TIMESTAMP` - : ''; + columnDefault = statement.columnDefault ? ` DEFAULT ${statement.columnDefault}` : ''; - columnAutoincrement = statement.columnAutoIncrement - ? ' AUTO_INCREMENT' - : ''; + // columnAutoincrement = statement.columnAutoIncrement + // ? ' AUTO_INCREMENT' + // : ''; if (statement.oldColumn?.generated?.type === 'virtual') { return [ @@ -3226,30 +3101,26 @@ class GoogleSqlModifyColumn extends Convertor { } else { columnType = ` ${statement.newDataType}`; columnNotNull = statement.columnNotNull ? ` NOT NULL` : ''; - columnOnUpdate = columnOnUpdate = statement.columnOnUpdate - ? ` ON UPDATE CURRENT_TIMESTAMP` - : ''; columnDefault = statement.columnDefault ? ` DEFAULT ${statement.columnDefault}` : ''; - columnAutoincrement = statement.columnAutoIncrement - ? ' AUTO_INCREMENT' - : ''; + // columnAutoincrement = statement.columnAutoIncrement + // ? ' AUTO_INCREMENT' + // : ''; columnGenerated = statement.columnGenerated - ? ` GENERATED ALWAYS AS (${statement.columnGenerated?.as}) ${statement.columnGenerated?.type.toUpperCase()}` + ? ` AS (${statement.columnGenerated?.as}) ${statement.columnGenerated?.type === 'stored' ? 'STORED' : ''}` : ''; } // Seems like getting value from simple json2 shanpshot makes dates be dates - columnDefault = columnDefault instanceof Date + columnDefault = columnDefault instanceof Date // TODO: SPANNER - what? ? columnDefault.toISOString() : columnDefault; - return `ALTER TABLE \`${tableName}\` MODIFY COLUMN \`${columnName}\`${columnType}${columnAutoincrement}${columnGenerated}${columnNotNull}${columnDefault}${columnOnUpdate};`; + return `ALTER TABLE \`${tableName}\` ALTER COLUMN \`${columnName}\`${columnType}${columnNotNull}${columnDefault}${columnGenerated};`; } } - class SingleStoreAlterTableAlterColumnAlterrGeneratedConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return ( @@ -3669,15 +3540,15 @@ class MySqlAlterTableCreateCompositePrimaryKeyConvertor extends Convertor { } } -// TODO: SPANNER - verify +// TODO: SPANNER - can we remove this? class GoogleSqlAlterTableCreateCompositePrimaryKeyConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'create_composite_pk' && dialect === 'googlesql'; } convert(statement: JsonCreateCompositePK) { - const { name, columns } = GoogleSqlSquasher.unsquashPK(statement.data); - return `ALTER TABLE \`${statement.tableName}\` ADD PRIMARY KEY(\`${columns.join('`,`')}\`);`; + throw new Error('Google Cloud Spanner does not support adding primary key to an already created table'); + return ``; } } @@ -3692,14 +3563,15 @@ class MySqlAlterTableDeleteCompositePrimaryKeyConvertor extends Convertor { } } +// TODO: SPANNER - remove this? class GoogleSqlAlterTableDeleteCompositePrimaryKeyConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'delete_composite_pk' && dialect === 'googlesql'; } convert(statement: JsonDeleteCompositePK) { - const { name, columns } = GoogleSqlSquasher.unsquashPK(statement.data); - return `ALTER TABLE \`${statement.tableName}\` DROP PRIMARY KEY;`; + throw new Error('Google Cloud Spanner does not support deleting primary key from an already created table'); + return ``; } } @@ -3717,18 +3589,15 @@ class MySqlAlterTableAlterCompositePrimaryKeyConvertor extends Convertor { } } -// TODO: SPANNER - verify +// TODO: SPANNER - remove this? class GoogleSqlAlterTableAlterCompositePrimaryKeyConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'alter_composite_pk' && dialect === 'googlesql'; } convert(statement: JsonAlterCompositePK) { - const { name, columns } = GoogleSqlSquasher.unsquashPK(statement.old); - const { name: newName, columns: newColumns } = GoogleSqlSquasher.unsquashPK( - statement.new, - ); - return `ALTER TABLE \`${statement.tableName}\` DROP PRIMARY KEY, ADD PRIMARY KEY(\`${newColumns.join('`,`')}\`);`; + throw new Error('Google Cloud Spanner does not support altering primary key'); + return ``; } } @@ -4009,14 +3878,12 @@ class GoogleSqlCreateForeignKeyConvertor extends Convertor { columnsFrom, columnsTo, onDelete, - onUpdate, } = GoogleSqlSquasher.unsquashFK(statement.data); const onDeleteStatement = onDelete ? ` ON DELETE ${onDelete}` : ''; - const onUpdateStatement = onUpdate ? ` ON UPDATE ${onUpdate}` : ''; const fromColumnsString = columnsFrom.map((it) => `\`${it}\``).join(','); const toColumnsString = columnsTo.map((it) => `\`${it}\``).join(','); - return `ALTER TABLE \`${tableFrom}\` ADD CONSTRAINT \`${name}\` FOREIGN KEY (${fromColumnsString}) REFERENCES \`${tableTo}\`(${toColumnsString})${onDeleteStatement}${onUpdateStatement};`; + return `ALTER TABLE \`${tableFrom}\` ADD CONSTRAINT \`${name}\` FOREIGN KEY (${fromColumnsString}) REFERENCES \`${tableTo}\`(${toColumnsString})${onDeleteStatement};`; } } @@ -4092,7 +3959,6 @@ class MySqlDeleteForeignKeyConvertor extends Convertor { } } -// TODO: SPANNER - verify class GoogleSqlDeleteForeignKeyConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'delete_reference' && dialect === 'googlesql'; @@ -4101,7 +3967,7 @@ class GoogleSqlDeleteForeignKeyConvertor extends Convertor { convert(statement: JsonDeleteReferenceStatement): string { const tableFrom = statement.tableName; // delete fk from renamed table case const { name } = GoogleSqlSquasher.unsquashFK(statement.data); - return `ALTER TABLE \`${tableFrom}\` DROP FOREIGN KEY \`${name}\`;\n`; + return `ALTER TABLE \`${tableFrom}\` DROP CONSTRAINT \`${name}\`;\n`; } } @@ -4186,7 +4052,7 @@ class CreateMySqlIndexConvertor extends Convertor { } } -// TODO: SPANNER - verify +// TODO: SPANNER - support NULL_FILTERED class CreateGoogleSqlIndexConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'create_index' && dialect === 'googlesql'; @@ -4387,7 +4253,6 @@ class MySqlDropIndexConvertor extends Convertor { } } -// TODO: SPANNER - verify class GoogleSqlDropIndexConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'drop_index' && dialect === 'googlesql'; @@ -4395,7 +4260,7 @@ class GoogleSqlDropIndexConvertor extends Convertor { convert(statement: JsonDropIndexStatement): string { const { name } = GoogleSqlSquasher.unsquashIdx(statement.data); - return `DROP INDEX \`${name}\` ON \`${statement.tableName}\`;`; + return `DROP INDEX \`${name}\`;`; } } @@ -4599,8 +4464,6 @@ class SingleStoreRecreateTableConvertor extends Convertor { } } -// TODO: SPANNER - add googlesql/spanner classes - const convertors: Convertor[] = []; convertors.push(new PgCreateTableConvertor()); convertors.push(new MySqlCreateTableConvertor()); diff --git a/drizzle-kit/src/utils.ts b/drizzle-kit/src/utils.ts index 85e8e43670..52803adcdf 100644 --- a/drizzle-kit/src/utils.ts +++ b/drizzle-kit/src/utils.ts @@ -13,6 +13,7 @@ import { backwardCompatiblePgSchema } from './serializer/pgSchema'; import { backwardCompatibleSingleStoreSchema } from './serializer/singlestoreSchema'; import { backwardCompatibleSqliteSchema } from './serializer/sqliteSchema'; import type { ProxyParams } from './serializer/studio'; +import { backwardCompatibleGooglesqlSchema } from './serializer/googlesqlSchema'; export type Proxy = (params: ProxyParams) => Promise; @@ -129,7 +130,7 @@ const validatorForDialect = (dialect: Dialect) => { case 'gel': return { validator: backwardCompatibleGelSchema, version: 1 }; case 'googlesql': - throw new Error('Not implemented'); // TODO: SPANNER + return { validator: backwardCompatibleGooglesqlSchema, version: 0 }; // TODO: SPANNER - add proper version here } }; diff --git a/drizzle-kit/tests/googlesql-checks.test.ts b/drizzle-kit/tests/googlesql-checks.test.ts new file mode 100644 index 0000000000..34dff02ea2 --- /dev/null +++ b/drizzle-kit/tests/googlesql-checks.test.ts @@ -0,0 +1,316 @@ +import { sql } from 'drizzle-orm'; +import { check, foreignKey, googlesqlTable, index, int64, string, uniqueIndex } from 'drizzle-orm/googlesql'; +import { expect, test } from 'vitest'; +import { diffTestSchemasGooglesql } from './schemaDiffer'; + +test('create table with check', async (t) => { + // TODO: SPANNER - clean up this test to look like mysql-checks 'create table with check' + const to = { + users: googlesqlTable('users', { + id: int64('id').primaryKey(), + age: int64('age'), + name: string('name', { length: 255 }), + lastName: string('lastName', { length: 255 }), + email: string('email', { length: 255 }), + }, (table) => [ + check('users_age_check', sql`${table.age} > 13`), + uniqueIndex('users_email_idx').on(table.email), + index('users_lastName_name_idx').on(table.lastName, table.name), + ]), + }; + + const { sqlStatements, statements } = await diffTestSchemasGooglesql({}, to, []); + + expect(statements.length).toBe(3); + expect(statements[0]).toStrictEqual({ + type: 'create_table', + tableName: 'users', + columns: [ + { + name: 'id', + notNull: true, + primaryKey: false, + type: 'int64', + }, + { + name: 'age', + notNull: false, + primaryKey: false, + type: 'int64', + }, + { + name: 'name', + notNull: false, + primaryKey: false, + type: 'string(255)', + }, + { + name: 'lastName', + notNull: false, + primaryKey: false, + type: 'string(255)', + }, + { + name: 'email', + notNull: false, + primaryKey: false, + type: 'string(255)', + }, + ], + compositePKs: [ + 'users_id;id', + ], + checkConstraints: ['users_age_check;\`users\`.\`age\` > 13'], + compositePkName: 'users_id', + schema: undefined, + internals: { + tables: {}, + indexes: {}, + }, + }); + + expect(sqlStatements.length).toBe(3); + expect(sqlStatements[0]).toBe(`CREATE TABLE \`users\` ( +\t\`id\` int64 NOT NULL, +\t\`age\` int64, +\t\`name\` string(255), +\t\`lastName\` string(255), +\t\`email\` string(255), +\tCONSTRAINT \`users_age_check\` CHECK(\`users\`.\`age\` > 13) +) PRIMARY KEY(\`id\`);`); + expect(sqlStatements[1]).toBe(`CREATE UNIQUE INDEX \`users_email_idx\` ON \`users\` (\`email\`);`); + expect(sqlStatements[2]).toBe(`CREATE INDEX \`users_lastName_name_idx\` ON \`users\` (\`lastName\`,\`name\`);`); +}); + +test('add check constraint to existing table', async (t) => { + const from = { + users: googlesqlTable('users', { + id: int64('id').primaryKey(), + age: int64('age'), + }), + }; + + const to = { + users: googlesqlTable('users', { + id: int64('id').primaryKey(), + age: int64('age'), + }, (table) => [ + check('some_check_name', sql`${table.age} > 21`), + ]), + }; + + const { sqlStatements, statements } = await diffTestSchemasGooglesql(from, to, []); + + expect(statements.length).toBe(1); + expect(statements[0]).toStrictEqual({ + type: 'create_check_constraint', + tableName: 'users', + data: 'some_check_name;\`users\`.\`age\` > 21', + schema: '', + }); + + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toBe( + `ALTER TABLE \`users\` ADD CONSTRAINT \`some_check_name\` CHECK (\`users\`.\`age\` > 21);`, + ); +}); + +test('drop check constraint in existing table', async (t) => { + const to = { + users: googlesqlTable('users', { + id: int64('id').primaryKey(), + age: int64('age'), + }), + }; + + const from = { + users: googlesqlTable('users', { + id: int64('id').primaryKey(), + age: int64('age'), + }, (table) => [ + check('some_check_name', sql`${table.age} > 21`), + ]), + }; + + const { sqlStatements, statements } = await diffTestSchemasGooglesql(from, to, []); + + expect(statements.length).toBe(1); + expect(statements[0]).toStrictEqual({ + type: 'delete_check_constraint', + tableName: 'users', + schema: '', + constraintName: 'some_check_name', + }); + + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toBe( + `ALTER TABLE \`users\` DROP CONSTRAINT \`some_check_name\`;`, + ); +}); + +test('rename check constraint', async (t) => { + const from = { + users: googlesqlTable('users', { + id: int64('id').primaryKey(), + age: int64('age'), + }, (table) => [ + check('some_check_name', sql`${table.age} > 21`), + ]), + }; + + const to = { + users: googlesqlTable('users', { + id: int64('id').primaryKey(), + age: int64('age'), + }, (table) => [ + check('new_check_name', sql`${table.age} > 21`), + ]), + }; + + const { sqlStatements, statements } = await diffTestSchemasGooglesql(from, to, []); + + expect(statements.length).toBe(2); + expect(statements[0]).toStrictEqual({ + constraintName: 'some_check_name', + schema: '', + tableName: 'users', + type: 'delete_check_constraint', + }); + expect(statements[1]).toStrictEqual({ + data: 'new_check_name;\`users\`.\`age\` > 21', + schema: '', + tableName: 'users', + type: 'create_check_constraint', + }); + + expect(sqlStatements.length).toBe(2); + expect(sqlStatements[0]).toBe( + `ALTER TABLE \`users\` DROP CONSTRAINT \`some_check_name\`;`, + ); + expect(sqlStatements[1]).toBe( + `ALTER TABLE \`users\` ADD CONSTRAINT \`new_check_name\` CHECK (\`users\`.\`age\` > 21);`, + ); +}); + +test('alter check constraint', async (t) => { + const from = { + users: googlesqlTable('users', { + id: int64('id').primaryKey(), + age: int64('age'), + }, (table) => [ + check('some_check_name', sql`${table.age} > 21`), + ]), + }; + + const to = { + users: googlesqlTable('users', { + id: int64('id').primaryKey(), + age: int64('age'), + }, (table) => [ + check('new_check_name', sql`${table.age} > 10`), + ]), + }; + + const { sqlStatements, statements } = await diffTestSchemasGooglesql(from, to, []); + expect(statements.length).toBe(2); + expect(statements[0]).toStrictEqual({ + constraintName: 'some_check_name', + schema: '', + tableName: 'users', + type: 'delete_check_constraint', + }); + expect(statements[1]).toStrictEqual({ + data: 'new_check_name;\`users\`.\`age\` > 10', + schema: '', + tableName: 'users', + type: 'create_check_constraint', + }); + + expect(sqlStatements.length).toBe(2); + expect(sqlStatements[0]).toBe( + `ALTER TABLE \`users\` DROP CONSTRAINT \`some_check_name\`;`, + ); + expect(sqlStatements[1]).toBe( + `ALTER TABLE \`users\` ADD CONSTRAINT \`new_check_name\` CHECK (\`users\`.\`age\` > 10);`, + ); +}); + +test('alter multiple check constraints', async (t) => { + const from = { + users: googlesqlTable('users', { + id: int64('id').primaryKey(), + age: int64('age'), + name: string('name', { length: 255 }), + }, (table) => [ + check('some_check_name_1', sql`${table.age} > 21`), + check('some_check_name_2', sql`${table.name} != 'Alex'`), + ]), + }; + + const to = { + users: googlesqlTable('users', { + id: int64('id').primaryKey(), + age: int64('age'), + name: string('name', { length: 255 }), + }, (table) => ({ + checkConstraint1: check('some_check_name_3', sql`${table.age} > 21`), + checkConstraint2: check('some_check_name_4', sql`${table.name} != 'Alex'`), + })), + }; + + const { sqlStatements, statements } = await diffTestSchemasGooglesql(from, to, []); + expect(statements.length).toBe(4); + expect(statements[0]).toStrictEqual({ + constraintName: 'some_check_name_1', + schema: '', + tableName: 'users', + type: 'delete_check_constraint', + }); + expect(statements[1]).toStrictEqual({ + constraintName: 'some_check_name_2', + schema: '', + tableName: 'users', + type: 'delete_check_constraint', + }); + expect(statements[2]).toStrictEqual({ + data: 'some_check_name_3;\`users\`.\`age\` > 21', + schema: '', + tableName: 'users', + type: 'create_check_constraint', + }); + expect(statements[3]).toStrictEqual({ + data: "some_check_name_4;\`users\`.\`name\` != 'Alex'", + schema: '', + tableName: 'users', + type: 'create_check_constraint', + }); + + expect(sqlStatements.length).toBe(4); + expect(sqlStatements[0]).toBe( + `ALTER TABLE \`users\` DROP CONSTRAINT \`some_check_name_1\`;`, + ); + expect(sqlStatements[1]).toBe( + `ALTER TABLE \`users\` DROP CONSTRAINT \`some_check_name_2\`;`, + ); + expect(sqlStatements[2]).toBe( + `ALTER TABLE \`users\` ADD CONSTRAINT \`some_check_name_3\` CHECK (\`users\`.\`age\` > 21);`, + ); + expect(sqlStatements[3]).toBe( + `ALTER TABLE \`users\` ADD CONSTRAINT \`some_check_name_4\` CHECK (\`users\`.\`name\` != \'Alex\');`, + ); +}); + +test('create checks with same names', async (t) => { + const to = { + users: googlesqlTable('users', { + id: int64('id').primaryKey(), + age: int64('age'), + name: string('name', { length: 255 }), + }, (table) => ({ + checkConstraint1: check('some_check_name', sql`${table.age} > 21`), + checkConstraint2: check('some_check_name', sql`${table.name} != 'Alex'`), + })), + }; + + await expect(diffTestSchemasGooglesql({}, to, [])).rejects.toThrowError(); +}); diff --git a/drizzle-kit/tests/googlesql-generated.test.ts b/drizzle-kit/tests/googlesql-generated.test.ts new file mode 100644 index 0000000000..0d4c8dd473 --- /dev/null +++ b/drizzle-kit/tests/googlesql-generated.test.ts @@ -0,0 +1,712 @@ +import { SQL, sql } from 'drizzle-orm'; +import { googlesqlTable, int64, string } from 'drizzle-orm/googlesql'; +import { expect, test } from 'vitest'; +import { diffTestSchemasGooglesql } from './schemaDiffer'; + +test('generated as callback: add column with generated constraint', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + (): SQL => sql`${to.users.name} || 'hello'`, + { mode: 'stored' }, + ), + }), + }; + + const { statements, sqlStatements } = await diffTestSchemasGooglesql( + from, + to, + [], + ); + + expect(statements).toStrictEqual([ + { + column: { + generated: { + as: "`name` || 'hello'", + type: 'stored', + }, + name: 'gen_name', + notNull: false, + primaryKey: false, + type: 'string(MAX)', + }, + schema: '', + tableName: 'users', + type: 'alter_table_add_column', + }, + ]); + expect(sqlStatements).toStrictEqual([ + "ALTER TABLE `users` ADD COLUMN `gen_name` string(MAX) AS (`name` || 'hello') STORED;", + ]); +}); + +test('generated as callback: add generated constraint to an exisiting column as stored - ERROR', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').notNull(), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name') + .notNull() + .generatedAlwaysAs((): SQL => sql`${from.users.name} || 'to add'`, { + mode: 'stored', + }), + }), + }; + + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + 'Google Cloud Spanner does not support transform an existing column to a generated column', + ); +}); + +test('generated as callback: add generated constraint to an exisiting column as virtual - ERROR', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').notNull(), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name') + .notNull() + .generatedAlwaysAs((): SQL => sql`${from.users.name} || 'to add'`, { + mode: 'virtual', + }), + }), + }; + + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + 'Google Cloud Spanner does not support transform an existing column to a generated column', + ); +}); + +test('generated as callback: drop generated constraint as stored - ERROR', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + (): SQL => sql`${from.users.name} || 'to delete'`, + { mode: 'stored' }, + ), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName1: string('gen_name'), + }), + }; + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + 'Google Cloud Spanner does not support transform an generated column to a non-generated column', + ); +}); + +test('generated as callback: drop generated constraint as virtual - ERROR', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + (): SQL => sql`${from.users.name} || 'to delete'`, + { mode: 'virtual' }, + ), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName1: string('gen_name'), + }), + }; + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + 'Google Cloud Spanner does not support transform an generated column to a non-generated column', + ); +}); + +test('generated as callback: change generated constraint type from virtual to stored - ERROR', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + (): SQL => sql`${from.users.name}`, + { mode: 'virtual' }, + ), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + (): SQL => sql`${to.users.name} || 'hello'`, + { mode: 'stored' }, + ), + }), + }; + + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + "Google Cloud Spanner doesn't support changing stored generated columns", + ); +}); + +test('generated as callback: change generated constraint type from stored to virtual - ERROR', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + (): SQL => sql`${from.users.name}`, + ), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + (): SQL => sql`${to.users.name} || 'hello'`, + ), + }), + }; + + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + "Google Cloud Spanner doesn't support changing stored generated columns", + ); +}); + +test('generated as callback: change generated constraint - ERROR', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + (): SQL => sql`${from.users.name}`, + ), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + (): SQL => sql`${to.users.name} || 'hello'`, + ), + }), + }; + + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + "Google Cloud Spanner doesn't support changing stored generated columns", + ); +}); + +// --- + +test('generated as sql: add column with generated constraint', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + sql`\`name\` || 'hello'`, + { mode: 'stored' }, + ), + }), + }; + + const { statements, sqlStatements } = await diffTestSchemasGooglesql( + from, + to, + [], + ); + + expect(statements).toStrictEqual([ + { + column: { + generated: { + as: "`name` || 'hello'", + type: 'stored', + }, + name: 'gen_name', + notNull: false, + primaryKey: false, + type: 'string(MAX)', + }, + schema: '', + tableName: 'users', + type: 'alter_table_add_column', + }, + ]); + expect(sqlStatements).toStrictEqual([ + "ALTER TABLE `users` ADD COLUMN `gen_name` string(MAX) AS (`name` || 'hello') STORED;", + ]); +}); + +test('generated as sql: add generated constraint to an exisiting column as stored', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').notNull(), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name') + .notNull() + .generatedAlwaysAs(sql`\`name\` || 'to add'`, { + mode: 'stored', + }), + }), + }; + + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + 'Google Cloud Spanner does not support transform an existing column to a generated column', + ); +}); + +test('generated as sql: add generated constraint to an exisiting column as virtual', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').notNull(), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name') + .notNull() + .generatedAlwaysAs(sql`\`name\` || 'to add'`, { + mode: 'virtual', + }), + }), + }; + + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + 'Google Cloud Spanner does not support transform an existing column to a generated column', + ); +}); + +test('generated as sql: drop generated constraint as stored', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + sql`\`name\` || 'to delete'`, + { mode: 'stored' }, + ), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName1: string('gen_name'), + }), + }; + + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + 'Google Cloud Spanner does not support transform an generated column to a non-generated column', + ); +}); + +test('generated as sql: drop generated constraint as virtual', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + sql`\`name\` || 'to delete'`, + { mode: 'virtual' }, + ), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName1: string('gen_name'), + }), + }; + + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + 'Google Cloud Spanner does not support transform an generated column to a non-generated column', + ); +}); + +test('generated as sql: change generated constraint type from virtual to stored', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + sql`\`name\``, + { mode: 'virtual' }, + ), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + sql`\`name\` || 'hello'`, + { mode: 'stored' }, + ), + }), + }; + + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + "Google Cloud Spanner doesn't support changing stored generated columns", + ); +}); + +test('generated as sql: change generated constraint type from stored to virtual', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + sql`\`name\``, + ), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + sql`\`name\` || 'hello'`, + ), + }), + }; + + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + "Google Cloud Spanner doesn't support changing stored generated columns", + ); +}); + +test('generated as sql: change generated constraint', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + sql`\`name\``, + ), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + sql`\`name\` || 'hello'`, + ), + }), + }; + + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + "Google Cloud Spanner doesn't support changing stored generated columns", + ); +}); + +// --- + +test('generated as string: add column with generated constraint', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + `\`name\` || 'hello'`, + { mode: 'stored' }, + ), + }), + }; + + const { statements, sqlStatements } = await diffTestSchemasGooglesql( + from, + to, + [], + ); + + expect(statements).toStrictEqual([ + { + column: { + generated: { + as: "`name` || 'hello'", + type: 'stored', + }, + name: 'gen_name', + notNull: false, + primaryKey: false, + type: 'string(MAX)', + }, + schema: '', + tableName: 'users', + type: 'alter_table_add_column', + }, + ]); + expect(sqlStatements).toStrictEqual([ + "ALTER TABLE `users` ADD COLUMN `gen_name` string(MAX) AS (`name` || 'hello') STORED;", + ]); +}); + +test('generated as string: add generated constraint to an exisiting column as stored', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').notNull(), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name') + .notNull() + .generatedAlwaysAs(`\`name\` || 'to add'`, { + mode: 'stored', + }), + }), + }; + + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + 'Google Cloud Spanner does not support transform an existing column to a generated column', + ); +}); + +test('generated as string: add generated constraint to an exisiting column as virtual', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').notNull(), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name') + .notNull() + .generatedAlwaysAs(`\`name\` || 'to add'`, { + mode: 'virtual', + }), + }), + }; + + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + 'Google Cloud Spanner does not support transform an existing column to a generated column', + ); +}); + +test('generated as string: drop generated constraint as stored', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + `\`name\` || 'to delete'`, + { mode: 'stored' }, + ), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName1: string('gen_name'), + }), + }; + + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + 'Google Cloud Spanner does not support transform an generated column to a non-generated column', + ); +}); + +test('generated as string: drop generated constraint as virtual', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + `\`name\` || 'to delete'`, + { mode: 'virtual' }, + ), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName1: string('gen_name'), + }), + }; + + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + 'Google Cloud Spanner does not support transform an generated column to a non-generated column', + ); +}); + +test('generated as string: change generated constraint type from virtual to stored', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs(`\`name\``, { + mode: 'virtual', + }), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + `\`name\` || 'hello'`, + { mode: 'stored' }, + ), + }), + }; + + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + "Google Cloud Spanner doesn't support changing stored generated columns", + ); +}); + +test('generated as string: change generated constraint type from stored to virtual', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs(`\`name\``), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + `\`name\` || 'hello'`, + ), + }), + }; + + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + "Google Cloud Spanner doesn't support changing stored generated columns", + ); +}); + +test('generated as string: change generated constraint', async () => { + const from = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs(`\`name\``), + }), + }; + const to = { + users: googlesqlTable('users', { + id: int64('id'), + id2: int64('id2'), + name: string('name'), + generatedName: string('gen_name').generatedAlwaysAs( + `\`name\` || 'hello'`, + ), + }), + }; + + await expect(diffTestSchemasGooglesql(from, to, [])).rejects.toThrowError( + "Google Cloud Spanner doesn't support changing stored generated columns", + ); +}); diff --git a/drizzle-kit/tests/googlesql-schemas.test.ts b/drizzle-kit/tests/googlesql-schemas.test.ts new file mode 100644 index 0000000000..7b66ef5f5c --- /dev/null +++ b/drizzle-kit/tests/googlesql-schemas.test.ts @@ -0,0 +1,155 @@ +import { googlesqlSchema, googlesqlTable } from 'drizzle-orm/googlesql'; +import { expect, test } from 'vitest'; +import { diffTestSchemasGooglesql } from './schemaDiffer'; + +// We don't manage databases(schemas) in GoogleSQL with Drizzle Kit +test('add schema #1', async () => { + const to = { + devSchema: googlesqlSchema('dev'), + }; + + const { statements } = await diffTestSchemasGooglesql({}, to, []); + + expect(statements.length).toBe(0); +}); + +test('add schema #2', async () => { + const from = { + devSchema: googlesqlSchema('dev'), + }; + const to = { + devSchema: googlesqlSchema('dev'), + devSchema2: googlesqlSchema('dev2'), + }; + + const { statements } = await diffTestSchemasGooglesql(from, to, []); + + expect(statements.length).toBe(0); +}); + +test('delete schema #1', async () => { + const from = { + devSchema: googlesqlSchema('dev'), + }; + + const { statements } = await diffTestSchemasGooglesql(from, {}, []); + + expect(statements.length).toBe(0); +}); + +test('delete schema #2', async () => { + const from = { + devSchema: googlesqlSchema('dev'), + devSchema2: googlesqlSchema('dev2'), + }; + const to = { + devSchema: googlesqlSchema('dev'), + }; + + const { statements } = await diffTestSchemasGooglesql(from, to, []); + + expect(statements.length).toBe(0); +}); + +test('rename schema #1', async () => { + const from = { + devSchema: googlesqlSchema('dev'), + }; + const to = { + devSchema2: googlesqlSchema('dev2'), + }; + + const { statements } = await diffTestSchemasGooglesql(from, to, ['dev->dev2']); + + expect(statements.length).toBe(0); +}); + +test('rename schema #2', async () => { + const from = { + devSchema: googlesqlSchema('dev'), + devSchema1: googlesqlSchema('dev1'), + }; + const to = { + devSchema: googlesqlSchema('dev'), + devSchema2: googlesqlSchema('dev2'), + }; + + const { statements } = await diffTestSchemasGooglesql(from, to, ['dev1->dev2']); + + expect(statements.length).toBe(0); +}); + +test('add table to schema #1', async () => { + const dev = googlesqlSchema('dev'); + const from = {}; + const to = { + dev, + users: dev.table('users', {}), + }; + + const { statements } = await diffTestSchemasGooglesql(from, to, ['dev1->dev2']); + + expect(statements.length).toBe(0); +}); + +test('add table to schema #2', async () => { + const dev = googlesqlSchema('dev'); + const from = { dev }; + const to = { + dev, + users: dev.table('users', {}), + }; + + const { statements } = await diffTestSchemasGooglesql(from, to, ['dev1->dev2']); + + expect(statements.length).toBe(0); +}); + +test('add table to schema #3', async () => { + const dev = googlesqlSchema('dev'); + const from = { dev }; + const to = { + dev, + usersInDev: dev.table('users', {}), + users: googlesqlTable('users', {}), + }; + + const { statements } = await diffTestSchemasGooglesql(from, to, ['dev1->dev2']); + + expect(statements.length).toBe(1); + expect(statements[0]).toStrictEqual({ + type: 'create_table', + tableName: 'users', + schema: undefined, + columns: [], + internals: { + tables: {}, + indexes: {}, + }, + compositePkName: '', + compositePKs: [], + checkConstraints: [], + }); +}); + +test('remove table from schema #1', async () => { + const dev = googlesqlSchema('dev'); + const from = { dev, users: dev.table('users', {}) }; + const to = { + dev, + }; + + const { statements } = await diffTestSchemasGooglesql(from, to, ['dev1->dev2']); + + expect(statements.length).toBe(0); +}); + +test('remove table from schema #2', async () => { + const dev = googlesqlSchema('dev'); + const from = { dev, users: dev.table('users', {}) }; + const to = {}; + + const { statements } = await diffTestSchemasGooglesql(from, to, ['dev1->dev2']); + + expect(statements.length).toBe(0); +}); diff --git a/drizzle-kit/tests/googlesql-views.test.ts b/drizzle-kit/tests/googlesql-views.test.ts new file mode 100644 index 0000000000..450b3b82ab --- /dev/null +++ b/drizzle-kit/tests/googlesql-views.test.ts @@ -0,0 +1,485 @@ +import { sql } from 'drizzle-orm'; +import { googlesqlTable, googlesqlView, int64 } from 'drizzle-orm/googlesql'; +import { expect, test } from 'vitest'; +import { diffTestSchemasGooglesql } from './schemaDiffer'; + +test('create view #1', async () => { + const users = googlesqlTable('users', { + id: int64('id').primaryKey().notNull(), + }); + + const from = { + users: users, + }; + const to = { + users: users, + view: googlesqlView('some_view').as((qb) => qb.select().from(users)), + }; + + const { statements, sqlStatements } = await diffTestSchemasGooglesql(from, to, []); + + expect(statements.length).toBe(1); + expect(statements[0]).toStrictEqual({ + type: 'googlesql_create_view', + name: 'some_view', + replace: false, + definition: 'select `id` from `users`', + sqlSecurity: 'definer', + }); + + expect(sqlStatements.length).toBe(1); + // TODO: SPANNER - warning: this query will not work in strict name resolution mode: "Alias id cannot be used without a qualifier in strict name resolution mode" + expect(sqlStatements[0]).toBe(`CREATE VIEW \`some_view\` +SQL SECURITY definer +AS (select \`id\` from \`users\`);`); +}); + +test('create view #2', async () => { + const users = googlesqlTable('users', { + id: int64('id').primaryKey().notNull(), + }); + + const from = { + users: users, + }; + const to = { + users: users, + view: googlesqlView('some_view', {}).sqlSecurity('definer').as(sql`SELECT * FROM ${users}`), + }; + + const { statements, sqlStatements } = await diffTestSchemasGooglesql(from, to, []); + + expect(statements.length).toBe(1); + expect(statements[0]).toStrictEqual({ + type: 'googlesql_create_view', + name: 'some_view', + replace: false, + definition: 'SELECT * FROM \`users\`', + sqlSecurity: 'definer', + }); + + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toBe(`CREATE VIEW \`some_view\` +SQL SECURITY definer +AS (SELECT * FROM \`users\`);`); +}); + +test('create view with existing flag', async () => { + const users = googlesqlTable('users', { + id: int64('id').primaryKey().notNull(), + }); + + const from = { + users: users, + }; + const to = { + users: users, + view: googlesqlView('some_view', {}).existing(), + }; + + const { statements, sqlStatements } = await diffTestSchemasGooglesql(from, to, []); + + expect(statements.length).toBe(0); + expect(sqlStatements.length).toBe(0); +}); + +test('drop view', async () => { + const users = googlesqlTable('users', { + id: int64('id').primaryKey().notNull(), + }); + + const from = { + users: users, + view: googlesqlView('some_view', {}).sqlSecurity('definer') + .as(sql`SELECT * FROM ${users}`), + }; + const to = { + users: users, + }; + + const { statements, sqlStatements } = await diffTestSchemasGooglesql(from, to, []); + + expect(statements.length).toBe(1); + expect(statements[0]).toStrictEqual({ + type: 'drop_view', + name: 'some_view', + }); + + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toBe(`DROP VIEW \`some_view\`;`); +}); + +test('drop view with existing flag', async () => { + const users = googlesqlTable('users', { + id: int64('id').primaryKey().notNull(), + }); + + const from = { + users: users, + view: googlesqlView('some_view', {}).sqlSecurity('definer') + .existing(), + }; + const to = { + users: users, + }; + + const { statements, sqlStatements } = await diffTestSchemasGooglesql(from, to, []); + + expect(statements.length).toBe(0); + expect(sqlStatements.length).toBe(0); +}); + +test('rename view - ERROR', async () => { + const users = googlesqlTable('users', { + id: int64('id').primaryKey().notNull(), + }); + + const from = { + users: users, + view: googlesqlView('some_view', {}).sqlSecurity('definer') + .as(sql`SELECT * FROM ${users}`), + }; + const to = { + users: users, + view: googlesqlView('new_some_view', {}).sqlSecurity('definer') + .as(sql`SELECT * FROM ${users}`), + }; + + await expect(diffTestSchemasGooglesql(from, to, [ + 'public.some_view->public.new_some_view', + ])).rejects.toThrowError( + 'Google Cloud Spanner does not support renaming views', + ); +}); + +test('rename view and alter meta options - ERROR', async () => { + const users = googlesqlTable('users', { + id: int64('id').primaryKey().notNull(), + }); + + const from = { + users: users, + view: googlesqlView('some_view', {}).sqlSecurity('definer') + .as(sql`SELECT * FROM ${users}`), + }; + const to = { + users: users, + view: googlesqlView('new_some_view', {}).sqlSecurity('definer') + .as(sql`SELECT * FROM ${users}`), + }; + + await expect(diffTestSchemasGooglesql(from, to, [ + 'public.some_view->public.new_some_view', + ])).rejects.toThrowError( + 'Google Cloud Spanner does not support renaming views', + ); +}); + +test('rename view with existing flag', async () => { + const users = googlesqlTable('users', { + id: int64('id').primaryKey().notNull(), + }); + + const from = { + users: users, + view: googlesqlView('some_view', {}).sqlSecurity('definer') + .existing(), + }; + const to = { + users: users, + view: googlesqlView('new_some_view', {}).sqlSecurity('definer') + .existing(), + }; + + const { statements, sqlStatements } = await diffTestSchemasGooglesql(from, to, [ + 'public.some_view->public.new_some_view', + ]); + + expect(statements.length).toBe(0); + expect(sqlStatements.length).toBe(0); +}); + +test('add meta to view', async () => { + const users = googlesqlTable('users', { + id: int64('id').primaryKey().notNull(), + }); + + const from = { + users: users, + view: googlesqlView('some_view', {}).as(sql`SELECT * FROM ${users}`), + }; + const to = { + users: users, + view: googlesqlView('some_view', {}).sqlSecurity('invoker') + .as(sql`SELECT * FROM ${users}`), + }; + + const { statements, sqlStatements } = await diffTestSchemasGooglesql(from, to, []); + + expect(statements.length).toBe(1); + expect(statements[0]).toStrictEqual({ + columns: {}, + definition: 'SELECT * FROM `users`', + isExisting: false, + name: 'some_view', + sqlSecurity: 'invoker', + type: 'alter_googlesql_view', + }); + + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toBe(`CREATE OR REPLACE VIEW \`some_view\` +SQL SECURITY invoker +AS (SELECT * FROM \`users\`);`); +}); + +test('add meta to view with existing flag', async () => { + const users = googlesqlTable('users', { + id: int64('id').primaryKey().notNull(), + }); + + const from = { + users: users, + view: googlesqlView('some_view', {}).existing(), + }; + const to = { + users: users, + view: googlesqlView('some_view', {}).sqlSecurity('definer') + .existing(), + }; + + const { statements, sqlStatements } = await diffTestSchemasGooglesql(from, to, []); + + expect(statements.length).toBe(0); + expect(sqlStatements.length).toBe(0); +}); + +test('alter meta to view', async () => { + const users = googlesqlTable('users', { + id: int64('id').primaryKey().notNull(), + }); + + const from = { + users: users, + view: googlesqlView('some_view', {}).sqlSecurity('invoker') + .as(sql`SELECT * FROM ${users}`), + }; + const to = { + users: users, + view: googlesqlView('some_view', {}).sqlSecurity('definer') + .as(sql`SELECT * FROM ${users}`), + }; + + const { statements, sqlStatements } = await diffTestSchemasGooglesql(from, to, []); + + expect(statements.length).toBe(1); + expect(statements[0]).toStrictEqual({ + columns: {}, + definition: 'SELECT * FROM `users`', + isExisting: false, + name: 'some_view', + sqlSecurity: 'definer', + type: 'alter_googlesql_view', + }); + + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toBe(`CREATE OR REPLACE VIEW \`some_view\` +SQL SECURITY definer +AS (SELECT * FROM \`users\`);`); +}); + +test('alter meta to view with existing flag', async () => { + const users = googlesqlTable('users', { + id: int64('id').primaryKey().notNull(), + }); + + const from = { + users: users, + view: googlesqlView('some_view', {}).sqlSecurity('invoker') + .existing(), + }; + const to = { + users: users, + view: googlesqlView('some_view', {}).sqlSecurity('definer') + .existing(), + }; + + const { statements, sqlStatements } = await diffTestSchemasGooglesql(from, to, []); + + expect(statements.length).toBe(0); + expect(sqlStatements.length).toBe(0); +}); + +test('drop meta from view', async () => { + const users = googlesqlTable('users', { + id: int64('id').primaryKey().notNull(), + }); + + const from = { + users: users, + view: googlesqlView('some_view', {}).sqlSecurity('invoker') + .as(sql`SELECT * FROM ${users}`), + }; + const to = { + users: users, + view: googlesqlView('some_view', {}).as(sql`SELECT * FROM ${users}`), + }; + + const { statements, sqlStatements } = await diffTestSchemasGooglesql(from, to, []); + + expect(statements.length).toBe(1); + expect(statements[0]).toStrictEqual({ + columns: {}, + definition: 'SELECT * FROM `users`', + isExisting: false, + name: 'some_view', + sqlSecurity: 'definer', + type: 'alter_googlesql_view', + }); + + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toBe(`CREATE OR REPLACE VIEW \`some_view\` +SQL SECURITY definer +AS (SELECT * FROM \`users\`);`); +}); + +test('drop meta from view existing flag', async () => { + const users = googlesqlTable('users', { + id: int64('id').primaryKey().notNull(), + }); + + const from = { + users: users, + + view: googlesqlView('some_view', {}).sqlSecurity('definer') + .existing(), + }; + const to = { + users: users, + view: googlesqlView('some_view', {}).existing(), + }; + + const { statements, sqlStatements } = await diffTestSchemasGooglesql(from, to, []); + + expect(statements.length).toBe(0); + expect(sqlStatements.length).toBe(0); +}); + +test('alter view ".as" value', async () => { + const users = googlesqlTable('users', { + id: int64('id').primaryKey().notNull(), + }); + + const from = { + users: users, + view: googlesqlView('some_view', {}).sqlSecurity('invoker') + .as(sql`SELECT * FROM ${users}`), + }; + const to = { + users: users, + view: googlesqlView('some_view', {}).sqlSecurity('invoker') + .as(sql`SELECT * FROM ${users} WHERE ${users.id} = 1`), + }; + + const { statements, sqlStatements } = await diffTestSchemasGooglesql(from, to, []); + + expect(statements.length).toBe(1); + expect(statements[0]).toStrictEqual({ + definition: 'SELECT * FROM `users` WHERE `users`.`id` = 1', + name: 'some_view', + sqlSecurity: 'invoker', + type: 'googlesql_create_view', + replace: true, + }); + + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toBe(`CREATE OR REPLACE VIEW \`some_view\` +SQL SECURITY invoker +AS (SELECT * FROM \`users\` WHERE \`users\`.\`id\` = 1);`); +}); + +test('rename and alter view ".as" value', async () => { + const users = googlesqlTable('users', { + id: int64('id').primaryKey().notNull(), + }); + + const from = { + users: users, + view: googlesqlView('some_view', {}).sqlSecurity('invoker') + .as(sql`SELECT * FROM ${users}`), + }; + const to = { + users: users, + view: googlesqlView('new_some_view', {}).sqlSecurity('invoker') + .as(sql`SELECT * FROM ${users} WHERE ${users.id} = 1`), + }; + + await expect(diffTestSchemasGooglesql(from, to, [ + 'public.some_view->public.new_some_view', + ])).rejects.toThrowError( + 'Google Cloud Spanner does not support renaming views', + ); +}); + +test('set existing', async () => { + const users = googlesqlTable('users', { + id: int64('id').primaryKey().notNull(), + }); + + const from = { + users: users, + view: googlesqlView('some_view', {}).sqlSecurity('invoker') + .as(sql`SELECT * FROM ${users}`), + }; + const to = { + users: users, + view: googlesqlView('new_some_view', {}).sqlSecurity('invoker') + .existing(), + }; + + const { statements, sqlStatements } = await diffTestSchemasGooglesql(from, to, [ + 'public.some_view->public.new_some_view', + ]); + + expect(statements.length).toBe(0); + expect(sqlStatements.length).toBe(0); +}); + +test('drop existing', async () => { + const users = googlesqlTable('users', { + id: int64('id').primaryKey().notNull(), + }); + + const from = { + users: users, + view: googlesqlView('some_view', {}).sqlSecurity('invoker') + .existing(), + }; + const to = { + users: users, + view: googlesqlView('new_some_view', {}).sqlSecurity('invoker') + .as(sql`SELECT * FROM ${users} WHERE ${users.id} = 1`), + }; + + const { statements, sqlStatements } = await diffTestSchemasGooglesql(from, to, [ + 'public.some_view->public.new_some_view', + ]); + + expect(statements.length).toBe(2); + expect(statements[0]).toStrictEqual({ + name: 'new_some_view', + type: 'drop_view', + }); + expect(statements[1]).toStrictEqual({ + definition: 'SELECT * FROM `users` WHERE `users`.`id` = 1', + name: 'new_some_view', + sqlSecurity: 'invoker', + type: 'googlesql_create_view', + replace: false, + }); + + expect(sqlStatements.length).toBe(2); + expect(sqlStatements[0]).toBe(`DROP VIEW \`new_some_view\`;`); // TODO: SPANNER - isn't this a bug that also exists in mysql-views.test.ts? shouldnt it be `DROP VIEW \`some_view\`;`? + expect(sqlStatements[1]).toBe(`CREATE VIEW \`new_some_view\` +SQL SECURITY invoker +AS (SELECT * FROM \`users\` WHERE \`users\`.\`id\` = 1);`); +}); diff --git a/drizzle-kit/tests/googlesql.test.ts b/drizzle-kit/tests/googlesql.test.ts new file mode 100644 index 0000000000..29eb05fa4b --- /dev/null +++ b/drizzle-kit/tests/googlesql.test.ts @@ -0,0 +1,785 @@ +import { sql } from 'drizzle-orm'; +import { + foreignKey, + googlesqlSchema, + googlesqlTable, + index, + int64, + json, + primaryKey, + string, + uniqueIndex, +} from 'drizzle-orm/googlesql'; +import { expect, test } from 'vitest'; +import { diffTestSchemasGooglesql } from './schemaDiffer'; + +test('add table #1', async () => { + const to = { + users: googlesqlTable('users', {}), + }; + + const { statements } = await diffTestSchemasGooglesql({}, to, []); + + expect(statements.length).toBe(1); + expect(statements[0]).toStrictEqual({ + type: 'create_table', + tableName: 'users', + schema: undefined, + columns: [], + compositePKs: [], + internals: { + tables: {}, + indexes: {}, + }, + compositePkName: '', + checkConstraints: [], + }); +}); + +test('add table #2', async () => { + const to = { + users: googlesqlTable('users', { + id: int64('id').primaryKey(), + }), + }; + + const { statements } = await diffTestSchemasGooglesql({}, to, []); + + expect(statements.length).toBe(1); + expect(statements[0]).toStrictEqual({ + type: 'create_table', + tableName: 'users', + schema: undefined, + columns: [ + { + name: 'id', + notNull: true, + primaryKey: false, + type: 'int64', + }, + ], + compositePKs: ['users_id;id'], + compositePkName: 'users_id', + checkConstraints: [], + internals: { + tables: {}, + indexes: {}, + }, + }); +}); + +test('add table #3', async () => { + const to = { + users: googlesqlTable( + 'users', + { + id: int64('id'), + }, + (t) => { + return { + pk: primaryKey({ + name: 'users_pk', + columns: [t.id], + }), + }; + }, + ), + }; + + const { statements } = await diffTestSchemasGooglesql({}, to, []); + + expect(statements.length).toBe(1); + expect(statements[0]).toStrictEqual({ + type: 'create_table', + tableName: 'users', + schema: undefined, + columns: [ + { + name: 'id', + notNull: true, + primaryKey: false, + type: 'int64', + }, + ], + compositePKs: ['users_pk;id'], + compositePkName: 'users_pk', + checkConstraints: [], + internals: { + tables: {}, + indexes: {}, + }, + }); +}); + +test('add table #4', async () => { + const to = { + users: googlesqlTable('users', {}), + posts: googlesqlTable('posts', {}), + }; + + const { statements } = await diffTestSchemasGooglesql({}, to, []); + + expect(statements.length).toBe(2); + expect(statements[0]).toStrictEqual({ + type: 'create_table', + tableName: 'users', + schema: undefined, + columns: [], + internals: { + tables: {}, + indexes: {}, + }, + compositePKs: [], + compositePkName: '', + checkConstraints: [], + }); + expect(statements[1]).toStrictEqual({ + type: 'create_table', + tableName: 'posts', + schema: undefined, + columns: [], + compositePKs: [], + internals: { + tables: {}, + indexes: {}, + }, + compositePkName: '', + checkConstraints: [], + }); +}); + +test('add table #5', async () => { + const schema = googlesqlSchema('folder'); + const from = { + schema, + }; + + const to = { + schema, + users: schema.table('users', {}), + }; + + const { statements } = await diffTestSchemasGooglesql(from, to, []); + + expect(statements.length).toBe(0); +}); + +test('add table #6', async () => { + const from = { + users1: googlesqlTable('users1', {}), + }; + + const to = { + users2: googlesqlTable('users2', {}), + }; + + const { statements } = await diffTestSchemasGooglesql(from, to, []); + + expect(statements.length).toBe(2); + expect(statements[0]).toStrictEqual({ + type: 'create_table', + tableName: 'users2', + schema: undefined, + columns: [], + internals: { + tables: {}, + indexes: {}, + }, + compositePKs: [], + compositePkName: '', + checkConstraints: [], + }); + expect(statements[1]).toStrictEqual({ + type: 'drop_table', + policies: [], + tableName: 'users1', + schema: undefined, + }); +}); + +test('add table #7', async () => { + const from = { + users1: googlesqlTable('users1', {}), + }; + + const to = { + users: googlesqlTable('users', {}), + users2: googlesqlTable('users2', {}), + }; + + const { statements } = await diffTestSchemasGooglesql(from, to, [ + 'public.users1->public.users2', + ]); + + expect(statements.length).toBe(2); + expect(statements[0]).toStrictEqual({ + type: 'create_table', + tableName: 'users', + schema: undefined, + columns: [], + compositePKs: [], + internals: { + tables: {}, + indexes: {}, + }, + compositePkName: '', + checkConstraints: [], + }); + expect(statements[1]).toStrictEqual({ + type: 'rename_table', + tableNameFrom: 'users1', + tableNameTo: 'users2', + fromSchema: undefined, + toSchema: undefined, + }); +}); + +test('add schema + table #1', async () => { + const schema = googlesqlSchema('folder'); + + const to = { + schema, + users: schema.table('users', {}), + }; + + const { statements } = await diffTestSchemasGooglesql({}, to, []); + + expect(statements.length).toBe(0); +}); + +test('change schema with tables #1', async () => { + const schema = googlesqlSchema('folder'); + const schema2 = googlesqlSchema('folder2'); + const from = { + schema, + users: schema.table('users', {}), + }; + const to = { + schema2, + users: schema2.table('users', {}), + }; + + const { statements } = await diffTestSchemasGooglesql(from, to, [ + 'folder->folder2', + ]); + + expect(statements.length).toBe(0); +}); + +test('change table schema #1', async () => { + const schema = googlesqlSchema('folder'); + const from = { + schema, + users: googlesqlTable('users', {}), + }; + const to = { + schema, + users: schema.table('users', {}), + }; + + const { statements } = await diffTestSchemasGooglesql(from, to, [ + 'public.users->folder.users', + ]); + + expect(statements.length).toBe(1); + expect(statements[0]).toStrictEqual({ + type: 'drop_table', + policies: [], + tableName: 'users', + schema: undefined, + }); +}); + +test('change table schema #2', async () => { + const schema = googlesqlSchema('folder'); + const from = { + schema, + users: schema.table('users', {}), + }; + const to = { + schema, + users: googlesqlTable('users', {}), + }; + + const { statements } = await diffTestSchemasGooglesql(from, to, [ + 'folder.users->public.users', + ]); + + expect(statements.length).toBe(1); + expect(statements[0]).toStrictEqual({ + type: 'create_table', + tableName: 'users', + schema: undefined, + columns: [], + compositePkName: '', + compositePKs: [], + checkConstraints: [], + internals: { + tables: {}, + indexes: {}, + }, + }); +}); + +test('change table schema #3', async () => { + const schema1 = googlesqlSchema('folder1'); + const schema2 = googlesqlSchema('folder2'); + const from = { + schema1, + schema2, + users: schema1.table('users', {}), + }; + const to = { + schema1, + schema2, + users: schema2.table('users', {}), + }; + + const { statements } = await diffTestSchemasGooglesql(from, to, [ + 'folder1.users->folder2.users', + ]); + + expect(statements.length).toBe(0); +}); + +test('change table schema #4', async () => { + const schema1 = googlesqlSchema('folder1'); + const schema2 = googlesqlSchema('folder2'); + const from = { + schema1, + users: schema1.table('users', {}), + }; + const to = { + schema1, + schema2, // add schema + users: schema2.table('users', {}), // move table + }; + + const { statements } = await diffTestSchemasGooglesql(from, to, [ + 'folder1.users->folder2.users', + ]); + + expect(statements.length).toBe(0); +}); + +test('change table schema #5', async () => { + const schema1 = googlesqlSchema('folder1'); + const schema2 = googlesqlSchema('folder2'); + const from = { + schema1, // remove schema + users: schema1.table('users', {}), + }; + const to = { + schema2, // add schema + users: schema2.table('users', {}), // move table + }; + + const { statements } = await diffTestSchemasGooglesql(from, to, [ + 'folder1.users->folder2.users', + ]); + + expect(statements.length).toBe(0); +}); + +test('change table schema #5', async () => { + const schema1 = googlesqlSchema('folder1'); + const schema2 = googlesqlSchema('folder2'); + const from = { + schema1, + schema2, + users: schema1.table('users', {}), + }; + const to = { + schema1, + schema2, + users: schema2.table('users2', {}), // rename and move table + }; + + const { statements } = await diffTestSchemasGooglesql(from, to, [ + 'folder1.users->folder2.users2', + ]); + + expect(statements.length).toBe(0); +}); + +test('change table schema #6', async () => { + const schema1 = googlesqlSchema('folder1'); + const schema2 = googlesqlSchema('folder2'); + const from = { + schema1, + users: schema1.table('users', {}), + }; + const to = { + schema2, // rename schema + users: schema2.table('users2', {}), // rename table + }; + + const { statements } = await diffTestSchemasGooglesql(from, to, [ + 'folder1->folder2', + 'folder2.users->folder2.users2', + ]); + + expect(statements.length).toBe(0); +}); + +test('add table #10', async () => { + const to = { + users: googlesqlTable('table', { + json: json('json').default({}), + }), + }; + + const { sqlStatements } = await diffTestSchemasGooglesql({}, to, []); + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toBe( + "CREATE TABLE `table` (\n\t`json` json DEFAULT (JSON '{}')\n);", + ); +}); + +test('add table #11', async () => { + const to = { + users: googlesqlTable('table', { + json: json('json').default([]), + }), + }; + + const { sqlStatements } = await diffTestSchemasGooglesql({}, to, []); + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toBe( + "CREATE TABLE `table` (\n\t`json` json DEFAULT (JSON '[]')\n);", + ); +}); + +test('add table #12', async () => { + const to = { + users: googlesqlTable('table', { + json: json('json').default([1, 2, 3]), + }), + }; + + const { sqlStatements } = await diffTestSchemasGooglesql({}, to, []); + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toBe( + "CREATE TABLE `table` (\n\t`json` json DEFAULT (JSON '[1,2,3]')\n);", + ); +}); + +test('add table #13', async () => { + const to = { + users: googlesqlTable('table', { + json: json('json').default({ key: 'value' }), + }), + }; + + const { sqlStatements } = await diffTestSchemasGooglesql({}, to, []); + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toBe( + 'CREATE TABLE `table` (\n\t`json` json DEFAULT (JSON \'{"key":"value"}\')\n);', + ); +}); + +test('add table #14', async () => { + const to = { + users: googlesqlTable('table', { + json: json('json').default({ + key: 'value', + arr: [1, 2, 3], + }), + }), + }; + + const { sqlStatements } = await diffTestSchemasGooglesql({}, to, []); + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toBe( + 'CREATE TABLE `table` (\n\t`json` json DEFAULT (JSON \'{"key":"value","arr":[1,2,3]}\')\n);', + ); +}); + +test('drop index', async () => { + const from = { + users: googlesqlTable( + 'table', + { + name: string('name'), + }, + (t) => { + return { + idx: index('name_idx').on(t.name), + }; + }, + ), + }; + + const to = { + users: googlesqlTable('table', { + name: string('name'), + }), + }; + + const { sqlStatements } = await diffTestSchemasGooglesql(from, to, []); + expect(sqlStatements.length).toBe(1); + expect(sqlStatements[0]).toBe('DROP INDEX `name_idx`;'); +}); + +test('add table with indexes', async () => { + const from = {}; + + const to = { + users: googlesqlTable( + 'users', + { + id: int64('id').primaryKey(), + name: string('name'), + email: string('email'), + }, + (t) => ({ + uniqueExpr: uniqueIndex('uniqueExpr').on(sql`${t.email}`), + indexExpr: index('indexExpr').on(sql`${t.email}`), + indexExprMultiple: index('indexExprMultiple').on( + sql`${t.email}`, + sql`${t.name}`, + ), + + uniqueCol: uniqueIndex('uniqueCol').on(t.email), + indexCol: index('indexCol').on(t.email), + indexColMultiple: index('indexColMultiple').on(t.email, t.name), + + indexColExpr: index('indexColExpr').on( + sql`${t.email}`, + t.name, + ), + }), + ), + }; + + const { sqlStatements } = await diffTestSchemasGooglesql(from, to, []); + expect(sqlStatements.length).toBe(8); + expect(sqlStatements).toStrictEqual([ + `CREATE TABLE \`users\` (\n\t\`id\` int64 NOT NULL,\n\t\`name\` string(MAX),\n\t\`email\` string(MAX)\n) PRIMARY KEY(\`id\`);`, + 'CREATE UNIQUE INDEX `uniqueExpr` ON `users` (`email`);', + 'CREATE INDEX `indexExpr` ON `users` (`email`);', + 'CREATE INDEX `indexExprMultiple` ON `users` (`email`,`name`);', + 'CREATE UNIQUE INDEX `uniqueCol` ON `users` (`email`);', + 'CREATE INDEX `indexCol` ON `users` (`email`);', + 'CREATE INDEX `indexColMultiple` ON `users` (`email`,`name`);', + 'CREATE INDEX `indexColExpr` ON `users` (`email`,`name`);', + ]); +}); + +test('string(size) and string default values escape single quotes', async (t) => { + const schema1 = { + table: googlesqlTable('table', { + id: int64('id').primaryKey(), + }), + }; + + const schem2 = { + table: googlesqlTable('table', { + id: int64('id').primaryKey(), + text: string('text').default("escape's quotes"), + textWithLen: string('textWithLen', { length: 255 }).default("escape's quotes"), + }), + }; + + const { sqlStatements } = await diffTestSchemasGooglesql(schema1, schem2, []); + + expect(sqlStatements.length).toBe(2); + expect(sqlStatements[0]).toStrictEqual( + "ALTER TABLE `table` ADD COLUMN `text` string(MAX) DEFAULT ('escape\\'s quotes');", + ); + expect(sqlStatements[1]).toStrictEqual( + "ALTER TABLE `table` ADD COLUMN `textWithLen` string(255) DEFAULT ('escape\\'s quotes');", + ); +}); + +test('composite primary key', async () => { + const from = {}; + const to = { + table: googlesqlTable('works_to_creators', { + workId: int64('work_id').notNull(), + creatorId: int64('creator_id').notNull(), + classification: string('classification').notNull(), + }, (t) => ({ + pk: primaryKey({ + columns: [t.workId, t.creatorId, t.classification], + }), + })), + }; + + const { sqlStatements } = await diffTestSchemasGooglesql(from, to, []); + + expect(sqlStatements).toStrictEqual([ + `CREATE TABLE \`works_to_creators\` ( +\t\`work_id\` int64 NOT NULL, +\t\`creator_id\` int64 NOT NULL, +\t\`classification\` string(MAX) NOT NULL +) PRIMARY KEY(\`work_id\`,\`creator_id\`,\`classification\`);`, + ]); +}); + +test('optional db aliases (snake case)', async () => { + const from = {}; + + const t1 = googlesqlTable( + 't1', + { + t1Id1: int64().notNull().primaryKey(), + t1Col2: int64().notNull(), + t1Col3: int64().notNull(), + t2Ref: int64().notNull().references(() => t2.t2Id), + t1Uni: int64().notNull(), + t1UniIdx: int64().notNull(), + t1Idx: int64().notNull(), + }, + (table) => ({ + uni: uniqueIndex('t1_uni').on(table.t1Uni), + uniIdx: uniqueIndex('t1_uni_idx').on(table.t1UniIdx), + idx: index('t1_idx').on(table.t1Idx), + fk: foreignKey({ + columns: [table.t1Col2, table.t1Col3], + foreignColumns: [t3.t3Id1, t3.t3Id2], + }), + }), + ); + + const t2 = googlesqlTable( + 't2', + { + t2Id: int64().primaryKey(), + }, + ); + + const t3 = googlesqlTable( + 't3', + { + t3Id1: int64(), + t3Id2: int64(), + }, + (table) => ({ + pk: primaryKey({ + columns: [table.t3Id1, table.t3Id2], + }), + }), + ); + + const to = { + t1, + t2, + t3, + }; + + const { sqlStatements } = await diffTestSchemasGooglesql(from, to, [], false, 'snake_case'); + + const st1 = `CREATE TABLE \`t1\` ( + \`t1_id1\` int64 NOT NULL, + \`t1_col2\` int64 NOT NULL, + \`t1_col3\` int64 NOT NULL, + \`t2_ref\` int64 NOT NULL, + \`t1_uni\` int64 NOT NULL, + \`t1_uni_idx\` int64 NOT NULL, + \`t1_idx\` int64 NOT NULL +) PRIMARY KEY(\`t1_id1\`);`; + + const st2 = `CREATE TABLE \`t2\` ( + \`t2_id\` int64 NOT NULL +) PRIMARY KEY(\`t2_id\`);`; + + const st3 = `CREATE TABLE \`t3\` ( + \`t3_id1\` int64 NOT NULL, + \`t3_id2\` int64 NOT NULL +) PRIMARY KEY(\`t3_id1\`,\`t3_id2\`);`; + + const st4 = + `ALTER TABLE \`t1\` ADD CONSTRAINT \`t1_t2_ref_t2_t2_id_fk\` FOREIGN KEY (\`t2_ref\`) REFERENCES \`t2\`(\`t2_id\`) ON DELETE no action;`; + + const st5 = + `ALTER TABLE \`t1\` ADD CONSTRAINT \`t1_t1_col2_t1_col3_t3_t3_id1_t3_id2_fk\` FOREIGN KEY (\`t1_col2\`,\`t1_col3\`) REFERENCES \`t3\`(\`t3_id1\`,\`t3_id2\`) ON DELETE no action;`; + + const st6 = 'CREATE UNIQUE INDEX `t1_uni` ON `t1` (`t1_uni`);'; + const st7 = 'CREATE UNIQUE INDEX `t1_uni_idx` ON `t1` (`t1_uni_idx`);'; + const st8 = `CREATE INDEX \`t1_idx\` ON \`t1\` (\`t1_idx\`);`; + + expect(sqlStatements).toStrictEqual([st1, st2, st3, st4, st5, st6, st7, st8]); +}); + +test('optional db aliases (camel case)', async () => { + const from = {}; + + const t1 = googlesqlTable( + 't1', + { + t1_id1: int64().notNull().primaryKey(), + t1_col2: int64().notNull(), + t1_col3: int64().notNull(), + t2_ref: int64().notNull().references(() => t2.t2_id), + t1_uni_idx: int64().notNull(), + t1_idx: int64().notNull(), + }, + (table) => ({ + uni_idx: uniqueIndex('t1UniIdx').on(table.t1_uni_idx), + idx: index('t1Idx').on(table.t1_idx), + fk: foreignKey({ + columns: [table.t1_col2, table.t1_col3], + foreignColumns: [t3.t3_id1, t3.t3_id2], + }), + }), + ); + + const t2 = googlesqlTable( + 't2', + { + t2_id: int64().primaryKey(), + }, + ); + + const t3 = googlesqlTable( + 't3', + { + t3_id1: int64(), + t3_id2: int64(), + }, + (table) => ({ + pk: primaryKey({ + columns: [table.t3_id1, table.t3_id2], + }), + }), + ); + + const to = { + t1, + t2, + t3, + }; + + const { sqlStatements } = await diffTestSchemasGooglesql(from, to, [], false, 'camelCase'); + + const st1 = `CREATE TABLE \`t1\` ( + \`t1Id1\` int64 NOT NULL, + \`t1Col2\` int64 NOT NULL, + \`t1Col3\` int64 NOT NULL, + \`t2Ref\` int64 NOT NULL, + \`t1UniIdx\` int64 NOT NULL, + \`t1Idx\` int64 NOT NULL +) PRIMARY KEY(\`t1Id1\`);`; + + const st2 = `CREATE TABLE \`t2\` ( + \`t2Id\` int64 NOT NULL +) PRIMARY KEY(\`t2Id\`);`; + + const st3 = `CREATE TABLE \`t3\` ( + \`t3Id1\` int64 NOT NULL, + \`t3Id2\` int64 NOT NULL +) PRIMARY KEY(\`t3Id1\`,\`t3Id2\`);`; + + const st4 = + `ALTER TABLE \`t1\` ADD CONSTRAINT \`t1_t2Ref_t2_t2Id_fk\` FOREIGN KEY (\`t2Ref\`) REFERENCES \`t2\`(\`t2Id\`) ON DELETE no action;`; + + const st5 = + `ALTER TABLE \`t1\` ADD CONSTRAINT \`t1_t1Col2_t1Col3_t3_t3Id1_t3Id2_fk\` FOREIGN KEY (\`t1Col2\`,\`t1Col3\`) REFERENCES \`t3\`(\`t3Id1\`,\`t3Id2\`) ON DELETE no action;`; + + const st6 = 'CREATE UNIQUE INDEX `t1UniIdx` ON `t1` (`t1UniIdx`);'; + + const st7 = `CREATE INDEX \`t1Idx\` ON \`t1\` (\`t1Idx\`);`; + + expect(sqlStatements).toStrictEqual([st1, st2, st3, st4, st5, st6, st7]); +}); diff --git a/drizzle-kit/tests/schemaDiffer.ts b/drizzle-kit/tests/schemaDiffer.ts index aa06a800fe..27c11786d7 100644 --- a/drizzle-kit/tests/schemaDiffer.ts +++ b/drizzle-kit/tests/schemaDiffer.ts @@ -2,6 +2,7 @@ import { PGlite } from '@electric-sql/pglite'; import { Client } from '@libsql/client/.'; import { Database } from 'better-sqlite3'; import { is } from 'drizzle-orm'; +import { GoogleSqlSchema, GoogleSqlTable, GoogleSqlView } from 'drizzle-orm/googlesql'; import { MySqlSchema, MySqlTable, MySqlView } from 'drizzle-orm/mysql-core'; import { getMaterializedViewConfig, @@ -27,6 +28,7 @@ import { libSqlLogSuggestionsAndReturn } from 'src/cli/commands/libSqlPushUtils' import { columnsResolver, enumsResolver, + googleSqlViewsResolver, indPolicyResolver, mySqlViewsResolver, Named, @@ -49,6 +51,8 @@ import { schemaToTypeScript } from 'src/introspect-pg'; import { schemaToTypeScript as schemaToTypeScriptSingleStore } from 'src/introspect-singlestore'; import { schemaToTypeScript as schemaToTypeScriptSQLite } from 'src/introspect-sqlite'; import { fromDatabase as fromGelDatabase } from 'src/serializer/gelSerializer'; +import { googlesqlSchema, squashGooglesqlScheme } from 'src/serializer/googlesqlSchema'; +import { generateGoogleSqlSnapshot } from 'src/serializer/googlesqlSerializer'; import { prepareFromMySqlImports } from 'src/serializer/mysqlImports'; import { mysqlSchema, squashMysqlScheme, ViewSquashed } from 'src/serializer/mysqlSchema'; import { fromDatabase as fromMySqlDatabase, generateMySqlSnapshot } from 'src/serializer/mysqlSerializer'; @@ -65,6 +69,7 @@ import { prepareFromSqliteImports } from 'src/serializer/sqliteImports'; import { sqliteSchema, squashSqliteScheme, View as SqliteView } from 'src/serializer/sqliteSchema'; import { fromDatabase as fromSqliteDatabase, generateSqliteSnapshot } from 'src/serializer/sqliteSerializer'; import { + applyGooglesqlSnapshotsDiff, applyLibSQLSnapshotsDiff, applyMysqlSnapshotsDiff, applyPgSnapshotsDiff, @@ -107,6 +112,10 @@ export type SinglestoreSchema = Record< string, SingleStoreTable | SingleStoreSchema /* | SingleStoreView */ >; +export type GooglesqlSchema = Record< + string, + GoogleSqlTable | GoogleSqlSchema | GoogleSqlView +>; export const testSchemasResolver = (renames: Set) => async (input: ResolverInput): Promise> => { @@ -791,6 +800,83 @@ async ( } }; +// TODO: SPANNER - verify +export const testViewsResolverGoogleSql = (renames: Set) => +async ( + input: ResolverInput, +): Promise> => { + try { + if ( + input.created.length === 0 + || input.deleted.length === 0 + || renames.size === 0 + ) { + return { + created: input.created, + moved: [], + renamed: [], + deleted: input.deleted, + }; + } + + let createdViews = [...input.created]; + let deletedViews = [...input.deleted]; + + const result: { + created: ViewSquashed[]; + moved: { name: string; schemaFrom: string; schemaTo: string }[]; + renamed: { from: ViewSquashed; to: ViewSquashed }[]; + deleted: ViewSquashed[]; + } = { created: [], renamed: [], deleted: [], moved: [] }; + + for (let rename of renames) { + const [from, to] = rename.split('->'); + + const idxFrom = deletedViews.findIndex((it) => { + return `${it.schema || 'public'}.${it.name}` === from; + }); + + if (idxFrom >= 0) { + const idxTo = createdViews.findIndex((it) => { + return `${it.schema || 'public'}.${it.name}` === to; + }); + + const viewFrom = deletedViews[idxFrom]; + const viewTo = createdViews[idxFrom]; + + if (viewFrom.schema !== viewTo.schema) { + result.moved.push({ + name: viewFrom.name, + schemaFrom: viewFrom.schema, + schemaTo: viewTo.schema, + }); + } + + if (viewFrom.name !== viewTo.name) { + result.renamed.push({ + from: deletedViews[idxFrom], + to: createdViews[idxTo], + }); + } + + delete createdViews[idxTo]; + delete deletedViews[idxFrom]; + + createdViews = createdViews.filter(Boolean); + deletedViews = deletedViews.filter(Boolean); + } + } + + result.created = createdViews; + result.deleted = deletedViews; + + return result; + } catch (e) { + console.error(e); + throw e; + } +}; + export const testViewsResolverSingleStore = (renames: Set) => async ( input: ResolverInput, @@ -1542,6 +1628,77 @@ export const diffTestSchemasMysql = async ( return { sqlStatements, statements }; }; +// TODO: SPANNER - verify +export const diffTestSchemasGooglesql = async ( + left: GooglesqlSchema, + right: GooglesqlSchema, + renamesArr: string[], + cli: boolean = false, + casing?: CasingType | undefined, +) => { + const leftTables = Object.values(left).filter((it) => is(it, GoogleSqlTable)) as GoogleSqlTable[]; + + const leftViews = Object.values(left).filter((it) => is(it, GoogleSqlView)) as GoogleSqlView[]; + + const rightTables = Object.values(right).filter((it) => is(it, GoogleSqlTable)) as GoogleSqlTable[]; + + const rightViews = Object.values(right).filter((it) => is(it, GoogleSqlView)) as GoogleSqlView[]; + + const serialized1 = generateGoogleSqlSnapshot(leftTables, leftViews, casing); + const serialized2 = generateGoogleSqlSnapshot(rightTables, rightViews, casing); + + const { version: v1, dialect: d1, ...rest1 } = serialized1; + const { version: v2, dialect: d2, ...rest2 } = serialized2; + + const sch1 = { + version: '0', + dialect: 'googlesql', + id: '0', + prevId: '0', + ...rest1, + } as const; + + const sch2 = { + version: '0', + dialect: 'googlesql', + id: '0', + prevId: '0', + ...rest2, + } as const; + + const sn1 = squashGooglesqlScheme(sch1); + const sn2 = squashGooglesqlScheme(sch2); + + const validatedPrev = googlesqlSchema.parse(sch1); + const validatedCur = googlesqlSchema.parse(sch2); + + const renames = new Set(renamesArr); + + if (!cli) { + const { sqlStatements, statements } = await applyGooglesqlSnapshotsDiff( + sn1, + sn2, + testTablesResolver(renames), + testColumnsResolver(renames), + testViewsResolverGoogleSql(renames), + validatedPrev, + validatedCur, + ); + return { sqlStatements, statements }; + } + + const { sqlStatements, statements } = await applyGooglesqlSnapshotsDiff( + sn1, + sn2, + tablesResolver, + columnsResolver, + googleSqlViewsResolver, + validatedPrev, + validatedCur, + ); + return { sqlStatements, statements }; +}; + export const diffTestSchemasSingleStore = async ( left: SinglestoreSchema, right: SinglestoreSchema, diff --git a/drizzle-kit/tests/validations.test.ts b/drizzle-kit/tests/validations.test.ts index 8a64603bb9..c26a5484b1 100644 --- a/drizzle-kit/tests/validations.test.ts +++ b/drizzle-kit/tests/validations.test.ts @@ -4,6 +4,8 @@ import { singlestoreCredentials } from 'src/cli/validations/singlestore'; import { sqliteCredentials } from 'src/cli/validations/sqlite'; import { expect, test } from 'vitest'; +// TODO: SPANNER - add tests for spanner + test('turso #1', () => { sqliteCredentials.parse({ dialect: 'sqlite', diff --git a/drizzle-orm/tests/casing/googlesql-to-snake.test.ts b/drizzle-orm/tests/casing/googlesql-to-snake.test.ts index f5df2fcabd..ab65c8177d 100644 --- a/drizzle-orm/tests/casing/googlesql-to-snake.test.ts +++ b/drizzle-orm/tests/casing/googlesql-to-snake.test.ts @@ -202,7 +202,7 @@ describe('googlesql to snake case', () => { params: ['John', 'Doe', 30], }); expect(db.dialect.casing.cache).toEqual(usersCache); - }); + }); it('insert (on duplicate key update)', ({ expect }) => { const query = db