diff --git a/drizzle-kit/src/cli/commands/migrate.ts b/drizzle-kit/src/cli/commands/migrate.ts index 8c62a5edb2..3d89d3f285 100644 --- a/drizzle-kit/src/cli/commands/migrate.ts +++ b/drizzle-kit/src/cli/commands/migrate.ts @@ -147,6 +147,29 @@ export const mySqlViewsResolver = async ( } }; +// TODO: SPANNER - verify +export const googleSqlViewsResolver = async ( + input: ResolverInput, +): Promise> => { + try { + const { created, deleted, moved, renamed } = await promptNamedWithSchemasConflict( + input.created, + input.deleted, + 'view', + ); + + return { + created: created, + deleted: deleted, + moved: moved, + renamed: renamed, + }; + } catch (e) { + console.error(e); + throw e; + } +}; + /* export const singleStoreViewsResolver = async ( input: ResolverInput, ): Promise> => { diff --git a/drizzle-kit/src/jsonStatements.ts b/drizzle-kit/src/jsonStatements.ts index b70d01b996..b53663a7e1 100644 --- a/drizzle-kit/src/jsonStatements.ts +++ b/drizzle-kit/src/jsonStatements.ts @@ -22,6 +22,7 @@ import { View as SqliteView, } from './serializer/sqliteSchema'; import { AlteredColumn, Column, Sequence, Table } from './snapshotsDiffer'; +import { GoogleSqlKitInternals, GoogleSqlSchema, GoogleSqlSquasher, View as GoogleSqlView } from './serializer/googlesqlSchema'; export interface JsonSqliteCreateTableStatement { type: 'sqlite_create_table'; @@ -682,6 +683,12 @@ export type JsonCreateMySqlViewStatement = { replace: boolean; } & Omit; + +export type JsonCreateGoogleSqlViewStatement = { + type: 'googlesql_create_view'; + replace: boolean; +} & Omit; + /* export type JsonCreateSingleStoreViewStatement = { type: 'singlestore_create_view'; replace: boolean; @@ -770,6 +777,10 @@ export type JsonAlterMySqlViewStatement = { type: 'alter_mysql_view'; } & Omit; +export type JsonAlterGoogleSqlViewStatement = { + type: 'alter_googlesql_view'; +} & Omit; + /* export type JsonAlterSingleStoreViewStatement = { type: 'alter_singlestore_view'; } & Omit; */ @@ -866,7 +877,9 @@ export type JsonStatement = | JsonIndRenamePolicyStatement | JsonDropIndPolicyStatement | JsonCreateIndPolicyStatement - | JsonAlterIndPolicyStatement; + | JsonAlterIndPolicyStatement + | JsonCreateGoogleSqlViewStatement + | JsonAlterGoogleSqlViewStatement; export const preparePgCreateTableJson = ( table: Table, @@ -927,6 +940,37 @@ export const prepareMySqlCreateTableJson = ( }; }; +// TODO: SPANNER - verify +export const prepareGoogleSqlCreateTableJson = ( + table: Table, + // TODO: remove? + json2: GoogleSqlSchema, + // we need it to know if some of the indexes(and in future other parts) are expressions or columns + // didn't change mysqlserialaizer, because it will break snapshots and diffs and it's hard to detect + // if previously it was an expression or column + internals: GoogleSqlKitInternals, +): JsonCreateTableStatement => { + const { name, schema, columns, compositePrimaryKeys, uniqueConstraints, checkConstraints } = table; + + return { + type: 'create_table', + tableName: name, + schema, + columns: Object.values(columns), + compositePKs: Object.values(compositePrimaryKeys), + compositePkName: Object.values(compositePrimaryKeys).length > 0 + ? json2.tables[name].compositePrimaryKeys[ + GoogleSqlSquasher.unsquashPK(Object.values(compositePrimaryKeys)[0]) + .name + ].name + : '', + uniqueConstraints: Object.values(uniqueConstraints), + internals, + checkConstraints: Object.values(checkConstraints), + }; +}; + + export const prepareSingleStoreCreateTableJson = ( table: Table, // TODO: remove? @@ -1686,6 +1730,363 @@ export const prepareAlterColumnsMysql = ( return [...dropPkStatements, ...setPkStatements, ...statements]; }; + +// TODO - SPANNER - verify +export const prepareAlterColumnsGooglesql = ( + tableName: string, + schema: string, + columns: AlteredColumn[], + // TODO: remove? + json1: CommonSquashedSchema, + json2: CommonSquashedSchema, + action?: 'push' | undefined, +): JsonAlterColumnStatement[] => { + let statements: JsonAlterColumnStatement[] = []; + let dropPkStatements: JsonAlterColumnDropPrimaryKeyStatement[] = []; + let setPkStatements: JsonAlterColumnSetPrimaryKeyStatement[] = []; + + for (const column of columns) { + const columnName = typeof column.name !== 'string' ? column.name.new : column.name; + + 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', + tableName, + columnName, + schema, + newDataType: columnType, + columnDefault, + columnOnUpdate, + columnNotNull, + columnAutoIncrement, + 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, + columnName, + schema, + newDataType: columnType, + columnDefault, + columnOnUpdate, + columnNotNull, + columnAutoIncrement, + columnPk, + }); + } + + if (column.autoincrement?.type === 'deleted') { + statements.push({ + type: 'alter_table_alter_column_drop_autoincrement', + tableName, + columnName, + schema, + newDataType: columnType, + columnDefault, + columnOnUpdate, + columnNotNull, + columnAutoIncrement, + columnPk, + }); + } + } + + for (const column of columns) { + const columnName = typeof column.name !== 'string' ? column.name.new : column.name; + + // I used any, because those fields are available only for mysql dialect + // For other dialects it will become undefined, that is fine for json statements + const columnType = json2.tables[tableName].columns[columnName].type; + const columnDefault = json2.tables[tableName].columns[columnName].default; + const columnGenerated = json2.tables[tableName].columns[columnName].generated; + const columnOnUpdate = (json2.tables[tableName].columns[columnName] as any) + .onUpdate; + const columnNotNull = json2.tables[tableName].columns[columnName].notNull; + const columnAutoIncrement = ( + json2.tables[tableName].columns[columnName] as any + ).autoincrement; + const columnPk = (json2.tables[tableName].columns[columnName] as any) + .primaryKey; + + const compositePk = json2.tables[tableName].compositePrimaryKeys[ + `${tableName}_${columnName}` + ]; + + if (typeof column.name !== 'string') { + statements.push({ + type: 'alter_table_rename_column', + tableName, + oldColumnName: column.name.old, + newColumnName: column.name.new, + schema, + }); + } + + if (column.type?.type === 'changed') { + statements.push({ + type: 'alter_table_alter_column_set_type', + tableName, + columnName, + newDataType: column.type.new, + oldDataType: column.type.old, + schema, + columnDefault, + columnOnUpdate, + columnNotNull, + columnAutoIncrement, + columnPk, + columnGenerated, + }); + } + + if ( + column.primaryKey?.type === 'deleted' + || (column.primaryKey?.type === 'changed' + && !column.primaryKey.new + && typeof compositePk === 'undefined') + ) { + dropPkStatements.push({ + //// + type: 'alter_table_alter_column_drop_pk', + tableName, + columnName, + schema, + }); + } + + if (column.default?.type === 'added') { + statements.push({ + type: 'alter_table_alter_column_set_default', + tableName, + columnName, + newDefaultValue: column.default.value, + schema, + columnOnUpdate, + columnNotNull, + columnAutoIncrement, + newDataType: columnType, + columnPk, + }); + } + + if (column.default?.type === 'changed') { + statements.push({ + type: 'alter_table_alter_column_set_default', + tableName, + columnName, + newDefaultValue: column.default.new, + oldDefaultValue: column.default.old, + schema, + columnOnUpdate, + columnNotNull, + columnAutoIncrement, + newDataType: columnType, + columnPk, + }); + } + + if (column.default?.type === 'deleted') { + statements.push({ + type: 'alter_table_alter_column_drop_default', + tableName, + columnName, + schema, + columnDefault, + columnOnUpdate, + columnNotNull, + columnAutoIncrement, + newDataType: columnType, + columnPk, + }); + } + + if (column.notNull?.type === 'added') { + statements.push({ + type: 'alter_table_alter_column_set_notnull', + tableName, + columnName, + schema, + newDataType: columnType, + columnDefault, + columnOnUpdate, + columnNotNull, + columnAutoIncrement, + columnPk, + }); + } + + if (column.notNull?.type === 'changed') { + const type = column.notNull.new + ? 'alter_table_alter_column_set_notnull' + : 'alter_table_alter_column_drop_notnull'; + statements.push({ + type: type, + tableName, + columnName, + schema, + newDataType: columnType, + columnDefault, + columnOnUpdate, + columnNotNull, + columnAutoIncrement, + columnPk, + }); + } + + if (column.notNull?.type === 'deleted') { + statements.push({ + type: 'alter_table_alter_column_drop_notnull', + tableName, + columnName, + schema, + newDataType: columnType, + columnDefault, + columnOnUpdate, + columnNotNull, + columnAutoIncrement, + columnPk, + }); + } + + if (column.generated?.type === 'added') { + if (columnGenerated?.type === 'virtual') { + warning( + `You are trying to add virtual generated constraint to ${ + chalk.blue( + columnName, + ) + } column. As MySQL docs mention: "Nongenerated columns can be altered to stored but not virtual generated columns". We will drop an existing column and add it with a virtual generated statement. This means that the data previously stored in this column will be wiped, and new data will be generated on each read for this column\n`, + ); + } + statements.push({ + type: 'alter_table_alter_column_set_generated', + tableName, + columnName, + schema, + newDataType: columnType, + columnDefault, + columnOnUpdate, + columnNotNull, + columnAutoIncrement, + columnPk, + columnGenerated, + }); + } + + if (column.generated?.type === 'changed' && action !== 'push') { + statements.push({ + type: 'alter_table_alter_column_alter_generated', + tableName, + columnName, + schema, + newDataType: columnType, + columnDefault, + columnOnUpdate, + columnNotNull, + columnAutoIncrement, + columnPk, + columnGenerated, + }); + } + + if (column.generated?.type === 'deleted') { + if (columnGenerated?.type === 'virtual') { + warning( + `You are trying to remove virtual generated constraint from ${ + chalk.blue( + columnName, + ) + } column. As MySQL docs mention: "Stored but not virtual generated columns can be altered to nongenerated columns. The stored generated values become the values of the nongenerated column". We will drop an existing column and add it without a virtual generated statement. This means that this column will have no data after migration\n`, + ); + } + statements.push({ + type: 'alter_table_alter_column_drop_generated', + tableName, + columnName, + schema, + newDataType: columnType, + columnDefault, + columnOnUpdate, + columnNotNull, + columnAutoIncrement, + columnPk, + columnGenerated, + oldColumn: json1.tables[tableName].columns[columnName], + }); + } + + if ( + column.primaryKey?.type === 'added' + || (column.primaryKey?.type === 'changed' && column.primaryKey.new) + ) { + const wasAutoincrement = statements.filter( + (it) => it.type === 'alter_table_alter_column_set_autoincrement', + ); + if (wasAutoincrement.length === 0) { + setPkStatements.push({ + type: 'alter_table_alter_column_set_pk', + tableName, + schema, + columnName, + }); + } + } + + if (column.onUpdate?.type === 'added') { + statements.push({ + type: 'alter_table_alter_column_set_on_update', + tableName, + columnName, + schema, + newDataType: columnType, + columnDefault, + columnOnUpdate, + columnNotNull, + columnAutoIncrement, + columnPk, + }); + } + + if (column.onUpdate?.type === 'deleted') { + statements.push({ + type: 'alter_table_alter_column_drop_on_update', + tableName, + columnName, + schema, + newDataType: columnType, + columnDefault, + columnOnUpdate, + columnNotNull, + columnAutoIncrement, + columnPk, + }); + } + } + + return [...dropPkStatements, ...setPkStatements, ...statements]; +}; + export const prepareAlterColumnsSingleStore = ( tableName: string, schema: string, @@ -3317,6 +3718,74 @@ export const prepareAlterCompositePrimaryKeyMySql = ( }); }; +// TODO - SPANNER - verify +export const prepareAddCompositePrimaryKeyGoogleSql = ( + tableName: string, + pks: Record, + // TODO: remove? + json1: GoogleSqlSchema, + json2: GoogleSqlSchema, +): JsonCreateCompositePK[] => { + const res: JsonCreateCompositePK[] = []; + for (const it of Object.values(pks)) { + const unsquashed = GoogleSqlSquasher.unsquashPK(it); + + if ( + unsquashed.columns.length === 1 + && json1.tables[tableName]?.columns[unsquashed.columns[0]]?.primaryKey + ) { + continue; + } + + res.push({ + type: 'create_composite_pk', + tableName, + data: it, + constraintName: unsquashed.name, + } as JsonCreateCompositePK); + } + return res; +}; + +export const prepareDeleteCompositePrimaryKeyGoogleSql = ( + tableName: string, + pks: Record, + // TODO: remove? + json1: GoogleSqlSchema, +): JsonDeleteCompositePK[] => { + return Object.values(pks).map((it) => { + const unsquashed = GoogleSqlSquasher.unsquashPK(it); + return { + type: 'delete_composite_pk', + tableName, + data: it, + } as JsonDeleteCompositePK; + }); +}; + +export const prepareAlterCompositePrimaryKeyGoogleSql = ( + tableName: string, + pks: Record, + // TODO: remove? + json1: GoogleSqlSchema, + json2: GoogleSqlSchema, +): JsonAlterCompositePK[] => { + return Object.values(pks).map((it) => { + return { + type: 'alter_composite_pk', + tableName, + old: it.__old, + new: it.__new, + oldConstraintName: json1.tables[tableName].compositePrimaryKeys[ + MySqlSquasher.unsquashPK(it.__old).name + ].name, + newConstraintName: json2.tables[tableName].compositePrimaryKeys[ + MySqlSquasher.unsquashPK(it.__new).name + ].name, + } as JsonAlterCompositePK; + }); +}; + export const preparePgCreateViewJson = ( name: string, schema: string, @@ -3358,6 +3827,25 @@ export const prepareMySqlCreateViewJson = ( }; }; +// TODO - SPANNER - verify +export const prepareGoogleSqlCreateViewJson = ( + name: string, + definition: string, + meta: string, + replace: boolean = false, +): JsonCreateGoogleSqlViewStatement => { + const { algorithm, sqlSecurity, withCheckOption } = GoogleSqlSquasher.unsquashView(meta); + return { + type: 'googlesql_create_view', + name: name, + definition: definition, + algorithm, + sqlSecurity, + withCheckOption, + replace, + }; +}; + /* export const prepareSingleStoreCreateViewJson = ( name: string, definition: string, @@ -3502,6 +3990,13 @@ export const prepareMySqlAlterView = ( return { type: 'alter_mysql_view', ...view }; }; +// TODO - SPANNER - verify +export const prepareGoogleSqlAlterView = ( + view: Omit, +): JsonAlterGoogleSqlViewStatement => { + return { type: 'alter_googlesql_view', ...view }; +}; + /* export const prepareSingleStoreAlterView = ( view: Omit, ): JsonAlterSingleStoreViewStatement => { diff --git a/drizzle-kit/src/schemaValidator.ts b/drizzle-kit/src/schemaValidator.ts index 0c6aa30de9..81506fc03e 100644 --- a/drizzle-kit/src/schemaValidator.ts +++ b/drizzle-kit/src/schemaValidator.ts @@ -3,6 +3,7 @@ import { mysqlSchema, mysqlSchemaSquashed } from './serializer/mysqlSchema'; import { pgSchema, pgSchemaSquashed } from './serializer/pgSchema'; import { singlestoreSchema, singlestoreSchemaSquashed } from './serializer/singlestoreSchema'; import { sqliteSchema, SQLiteSchemaSquashed } from './serializer/sqliteSchema'; +import { googlesqlSchemaSquashed } from './serializer/googlesqlSchema'; export const dialects = ['postgresql', 'mysql', 'sqlite', 'turso', 'singlestore', 'gel', 'googlesql'] as const; export const dialect = enumType(dialects); @@ -15,6 +16,7 @@ const commonSquashedSchema = union([ mysqlSchemaSquashed, SQLiteSchemaSquashed, singlestoreSchemaSquashed, + googlesqlSchemaSquashed, ]); // TODO: SPANNER SCHEMA? diff --git a/drizzle-kit/src/serializer/googlesqlSchema.ts b/drizzle-kit/src/serializer/googlesqlSchema.ts new file mode 100644 index 0000000000..f73f294a17 --- /dev/null +++ b/drizzle-kit/src/serializer/googlesqlSchema.ts @@ -0,0 +1,425 @@ +import { any, boolean, enum as enumType, literal, object, record, string, TypeOf, union } from 'zod'; +import { mapValues, originUUID } from '../global'; + +// TODO: SPANNER - verify + +// ------- V3 -------- +const index = object({ + name: string(), + columns: string().array(), + isUnique: boolean(), + using: enumType(['btree', 'hash']).optional(), + algorithm: enumType(['default', 'inplace', 'copy']).optional(), + lock: enumType(['default', 'none', 'shared', 'exclusive']).optional(), +}).strict(); + +const fk = object({ + name: string(), + tableFrom: string(), + columnsFrom: string().array(), + tableTo: string(), + columnsTo: string().array(), + onUpdate: string().optional(), + onDelete: string().optional(), +}).strict(); + +const column = object({ + name: string(), + type: string(), + primaryKey: boolean(), + notNull: boolean(), + autoincrement: boolean().optional(), + default: any().optional(), + onUpdate: any().optional(), + generated: object({ + type: enumType(['stored', 'virtual']), + as: string(), + }).optional(), +}).strict(); + +const tableV3 = object({ + name: string(), + columns: record(string(), column), + indexes: record(string(), index), + foreignKeys: record(string(), fk), +}).strict(); + +const compositePK = object({ + name: string(), + columns: string().array(), +}).strict(); + +const uniqueConstraint = object({ + name: string(), + columns: string().array(), +}).strict(); + +const checkConstraint = object({ + name: string(), + value: string(), +}).strict(); + +const tableV4 = object({ + name: string(), + schema: string().optional(), + columns: record(string(), column), + indexes: record(string(), index), + foreignKeys: record(string(), fk), +}).strict(); + +const table = object({ + name: string(), + columns: record(string(), column), + indexes: record(string(), index), + foreignKeys: record(string(), fk), + compositePrimaryKeys: record(string(), compositePK), + uniqueConstraints: record(string(), uniqueConstraint).default({}), + checkConstraint: record(string(), checkConstraint).default({}), +}).strict(); + +const viewMeta = object({ + algorithm: enumType(['undefined', 'merge', 'temptable']), + sqlSecurity: enumType(['definer', 'invoker']), + withCheckOption: enumType(['local', 'cascaded']).optional(), +}).strict(); + +export const view = object({ + name: string(), + columns: record(string(), column), + definition: string().optional(), + isExisting: boolean(), +}).strict().merge(viewMeta); +type SquasherViewMeta = Omit, 'definer'>; + +export const kitInternals = object({ + tables: record( + string(), + object({ + columns: record( + string(), + object({ isDefaultAnExpression: boolean().optional() }).optional(), + ), + }).optional(), + ).optional(), + indexes: record( + string(), + object({ + columns: record( + string(), + object({ isExpression: boolean().optional() }).optional(), + ), + }).optional(), + ).optional(), +}).optional(); + +// use main dialect +const dialect = literal('googlesql'); + +const schemaHash = object({ + id: string(), + prevId: string(), +}); + +export const schemaInternalV3 = object({ + version: literal('3'), + dialect: dialect, + tables: record(string(), tableV3), +}).strict(); + +export const schemaInternalV4 = object({ + version: literal('4'), + dialect: dialect, + tables: record(string(), tableV4), + schemas: record(string(), string()), +}).strict(); + +export const schemaInternalV5 = object({ + version: literal('5'), + dialect: dialect, + tables: record(string(), table), + schemas: record(string(), string()), + _meta: object({ + schemas: record(string(), string()), + tables: record(string(), string()), + columns: record(string(), string()), + }), + internal: kitInternals, +}).strict(); + +export const schemaInternal = object({ + version: literal('5'), + dialect: dialect, + tables: record(string(), table), + views: record(string(), view).default({}), + _meta: object({ + tables: record(string(), string()), + columns: record(string(), string()), + }), + internal: kitInternals, +}).strict(); + +export const schemaV3 = schemaInternalV3.merge(schemaHash); +export const schemaV4 = schemaInternalV4.merge(schemaHash); +export const schemaV5 = schemaInternalV5.merge(schemaHash); +export const schema = schemaInternal.merge(schemaHash); + +const tableSquashedV4 = object({ + name: string(), + schema: string().optional(), + columns: record(string(), column), + indexes: record(string(), string()), + foreignKeys: record(string(), string()), +}).strict(); + +const tableSquashed = object({ + name: string(), + columns: record(string(), column), + indexes: record(string(), string()), + foreignKeys: record(string(), string()), + compositePrimaryKeys: record(string(), string()), + uniqueConstraints: record(string(), string()).default({}), + checkConstraints: record(string(), string()).default({}), +}).strict(); + +const viewSquashed = view.omit({ + algorithm: true, + sqlSecurity: true, + withCheckOption: true, +}).extend({ meta: string() }); + +export const schemaSquashed = object({ + version: literal('5'), + dialect: dialect, + tables: record(string(), tableSquashed), + views: record(string(), viewSquashed), +}).strict(); + +export const schemaSquashedV4 = object({ + version: literal('4'), + dialect: dialect, + tables: record(string(), tableSquashedV4), + schemas: record(string(), string()), +}).strict(); + +export type Dialect = TypeOf; +export type Column = TypeOf; +export type Table = TypeOf; +export type TableV4 = TypeOf; +export type GoogleSqlSchema = TypeOf; +export type GoogleSqlSchemaV3 = TypeOf; +export type GoogleSqlSchemaV4 = TypeOf; +export type GoogleSqlSchemaV5 = TypeOf; +export type GoogleSqlSchemaInternal = TypeOf; +export type GoogleSqlKitInternals = TypeOf; +export type GoogleSqlSchemaSquashed = TypeOf; +export type GoogleSqlSchemaSquashedV4 = TypeOf; +export type Index = TypeOf; +export type ForeignKey = TypeOf; +export type PrimaryKey = TypeOf; +export type UniqueConstraint = TypeOf; +export type CheckConstraint = TypeOf; +export type View = TypeOf; +export type ViewSquashed = TypeOf; + +export const GoogleSqlSquasher = { + squashIdx: (idx: Index) => { + index.parse(idx); + return `${idx.name};${idx.columns.join(',')};${idx.isUnique};${idx.using ?? ''};${idx.algorithm ?? ''};${ + idx.lock ?? '' + }`; + }, + unsquashIdx: (input: string): Index => { + const [name, columnsString, isUnique, using, algorithm, lock] = input.split(';'); + const destructed = { + name, + columns: columnsString.split(','), + isUnique: isUnique === 'true', + using: using ? using : undefined, + algorithm: algorithm ? algorithm : undefined, + lock: lock ? lock : undefined, + }; + return index.parse(destructed); + }, + squashPK: (pk: PrimaryKey) => { + return `${pk.name};${pk.columns.join(',')}`; + }, + unsquashPK: (pk: string): PrimaryKey => { + const splitted = pk.split(';'); + return { name: splitted[0], columns: splitted[1].split(',') }; + }, + squashUnique: (unq: UniqueConstraint) => { + return `${unq.name};${unq.columns.join(',')}`; + }, + unsquashUnique: (unq: string): UniqueConstraint => { + const [name, columns] = unq.split(';'); + return { name, columns: columns.split(',') }; + }, + squashFK: (fk: ForeignKey) => { + return `${fk.name};${fk.tableFrom};${fk.columnsFrom.join(',')};${fk.tableTo};${fk.columnsTo.join(',')};${ + fk.onUpdate ?? '' + };${fk.onDelete ?? ''}`; + }, + unsquashFK: (input: string): ForeignKey => { + const [ + name, + tableFrom, + columnsFromStr, + tableTo, + columnsToStr, + onUpdate, + onDelete, + ] = input.split(';'); + + const result: ForeignKey = fk.parse({ + name, + tableFrom, + columnsFrom: columnsFromStr.split(','), + tableTo, + columnsTo: columnsToStr.split(','), + onUpdate, + onDelete, + }); + return result; + }, + squashCheck: (input: CheckConstraint): string => { + return `${input.name};${input.value}`; + }, + unsquashCheck: (input: string): CheckConstraint => { + const [name, value] = input.split(';'); + + return { name, value }; + }, + squashView: (view: View): string => { + return `${view.algorithm};${view.sqlSecurity};${view.withCheckOption}`; + }, + unsquashView: (meta: string): SquasherViewMeta => { + const [algorithm, sqlSecurity, withCheckOption] = meta.split(';'); + const toReturn = { + algorithm: algorithm, + sqlSecurity: sqlSecurity, + withCheckOption: withCheckOption !== 'undefined' ? withCheckOption : undefined, + }; + + return viewMeta.parse(toReturn); + }, +}; + +export const squashGooglesqlSchemeV4 = ( + json: GoogleSqlSchemaV4, +): GoogleSqlSchemaSquashedV4 => { + const mappedTables = Object.fromEntries( + Object.entries(json.tables).map((it) => { + const squashedIndexes = mapValues(it[1].indexes, (index) => { + return GoogleSqlSquasher.squashIdx(index); + }); + + const squashedFKs = mapValues(it[1].foreignKeys, (fk) => { + return GoogleSqlSquasher.squashFK(fk); + }); + + return [ + it[0], + { + name: it[1].name, + schema: it[1].schema, + columns: it[1].columns, + indexes: squashedIndexes, + foreignKeys: squashedFKs, + }, + ]; + }), + ); + return { + version: '4', + dialect: json.dialect, + tables: mappedTables, + schemas: json.schemas, + }; +}; + +export const squashGooglesqlScheme = (json: GoogleSqlSchema): GoogleSqlSchemaSquashed => { + const mappedTables = Object.fromEntries( + Object.entries(json.tables).map((it) => { + const squashedIndexes = mapValues(it[1].indexes, (index) => { + return GoogleSqlSquasher.squashIdx(index); + }); + + const squashedFKs = mapValues(it[1].foreignKeys, (fk) => { + return GoogleSqlSquasher.squashFK(fk); + }); + + const squashedPKs = mapValues(it[1].compositePrimaryKeys, (pk) => { + return GoogleSqlSquasher.squashPK(pk); + }); + + const squashedUniqueConstraints = mapValues( + it[1].uniqueConstraints, + (unq) => { + return GoogleSqlSquasher.squashUnique(unq); + }, + ); + + const squashedCheckConstraints = mapValues(it[1].checkConstraint, (check) => { + return GoogleSqlSquasher.squashCheck(check); + }); + + return [ + it[0], + { + name: it[1].name, + columns: it[1].columns, + indexes: squashedIndexes, + foreignKeys: squashedFKs, + compositePrimaryKeys: squashedPKs, + uniqueConstraints: squashedUniqueConstraints, + checkConstraints: squashedCheckConstraints, + }, + ]; + }), + ); + + const mappedViews = Object.fromEntries( + Object.entries(json.views).map(([key, value]) => { + const meta = GoogleSqlSquasher.squashView(value); + + return [key, { + name: value.name, + isExisting: value.isExisting, + columns: value.columns, + definition: value.definition, + meta, + }]; + }), + ); + + return { + version: '5', + dialect: json.dialect, + tables: mappedTables, + views: mappedViews, + }; +}; + +export const googlesqlSchema = schema; +export const googlesqlSchemaV3 = schemaV3; +export const googlesqlSchemaV4 = schemaV4; +export const googlesqlSchemaV5 = schemaV5; +export const googlesqlSchemaSquashed = schemaSquashed; + +// no prev version +export const backwardCompatibleGooglesqlSchema = union([googlesqlSchemaV5, schema]); + +export const dryGoogleSql = googlesqlSchema.parse({ + version: '5', + dialect: 'googlesql', + id: originUUID, + prevId: '', + tables: {}, + schemas: {}, + views: {}, + _meta: { + schemas: {}, + tables: {}, + columns: {}, + }, +}); diff --git a/drizzle-kit/src/serializer/googlesqlSerializer.ts b/drizzle-kit/src/serializer/googlesqlSerializer.ts new file mode 100644 index 0000000000..af783d27e0 --- /dev/null +++ b/drizzle-kit/src/serializer/googlesqlSerializer.ts @@ -0,0 +1,1002 @@ +import chalk from 'chalk'; +import { getTableName, is, SQL } from 'drizzle-orm'; +import { + AnyGoogleSqlTable, + getTableConfig, + getViewConfig, + GoogleSqlColumn, + GoogleSqlDialect, + GoogleSqlView, + type PrimaryKey as PrimaryKeyORM, + uniqueKeyName, +} from 'drizzle-orm/googlesql'; +import { RowDataPacket } from 'mysql2/promise'; +import { CasingType } from 'src/cli/validations/common'; +import { withStyle } from '../cli/validations/outputs'; +import { IntrospectStage, IntrospectStatus } from '../cli/views'; +import { + CheckConstraint, + Column, + ForeignKey, + Index, + GoogleSqlKitInternals, + GoogleSqlSchemaInternal, + PrimaryKey, + Table, + UniqueConstraint, + View, +} from '../serializer/googlesqlSchema'; +import { type DB, escapeSingleQuotes } from '../utils'; +import { getColumnCasing, sqlToStr } from './utils'; + +// TODO: SPANNER - verify + + +export const indexName = (tableName: string, columns: string[]) => { + return `${tableName}_${columns.join('_')}_index`; +}; + +const handleEnumType = (type: string) => { + let str = type.split('(')[1]; + str = str.substring(0, str.length - 1); + const values = str.split(',').map((v) => `'${escapeSingleQuotes(v.substring(1, v.length - 1))}'`); + return `enum(${values.join(',')})`; +}; + +export const generateGoogleSqlSnapshot = ( + tables: AnyGoogleSqlTable[], + views: GoogleSqlView[], + casing: CasingType | undefined, +): GoogleSqlSchemaInternal => { + const dialect = new GoogleSqlDialect({ casing }); + const result: Record = {}; + const resultViews: Record = {}; + const internal: GoogleSqlKitInternals = { tables: {}, indexes: {} }; + + for (const table of tables) { + const { + name: tableName, + columns, + indexes, + foreignKeys, + schema, + checks, + primaryKeys, + uniqueConstraints, + } = getTableConfig(table); + + const columnsObject: Record = {}; + const indexesObject: Record = {}; + const foreignKeysObject: Record = {}; + const primaryKeysObject: Record = {}; + const uniqueConstraintObject: Record = {}; + const checkConstraintObject: Record = {}; + + // this object will help to identify same check names + let checksInTable: Record = {}; + + columns.forEach((column) => { + const name = getColumnCasing(column, casing); + const notNull: boolean = column.notNull; + const sqlType = column.getSQLType(); + const sqlTypeLowered = sqlType.toLowerCase(); + const autoIncrement = typeof (column as any).autoIncrement === 'undefined' + ? false + : (column as any).autoIncrement; + + const generated = column.generated; + + const columnToSet: Column = { + name, + type: sqlType.startsWith('enum') ? handleEnumType(sqlType) : sqlType, + primaryKey: false, + // If field is autoincrement it's notNull by default + // notNull: autoIncrement ? true : notNull, + notNull, + autoincrement: autoIncrement, + onUpdate: (column as any).hasOnUpdateNow, + generated: generated + ? { + as: is(generated.as, SQL) + ? dialect.sqlToQuery(generated.as as SQL).sql + : typeof generated.as === 'function' + ? dialect.sqlToQuery(generated.as() as SQL).sql + : (generated.as as any), + type: generated.mode ?? 'stored', + } + : undefined, + }; + + if (column.primary) { + primaryKeysObject[`${tableName}_${name}`] = { + name: `${tableName}_${name}`, + columns: [name], + }; + } + + if (column.isUnique) { + const existingUnique = uniqueConstraintObject[column.uniqueName!]; + if (typeof existingUnique !== 'undefined') { + console.log( + `\n${ + withStyle.errorWarning(`We\'ve found duplicated unique constraint names in ${ + chalk.underline.blue( + tableName, + ) + } table. + The unique constraint ${ + chalk.underline.blue( + column.uniqueName, + ) + } on the ${ + chalk.underline.blue( + name, + ) + } column is confilcting with a unique constraint name already defined for ${ + chalk.underline.blue( + existingUnique.columns.join(','), + ) + } columns\n`) + }`, + ); + process.exit(1); + } + uniqueConstraintObject[column.uniqueName!] = { + name: column.uniqueName!, + columns: [columnToSet.name], + }; + } + + if (column.default !== undefined) { + if (is(column.default, SQL)) { + columnToSet.default = sqlToStr(column.default, casing); + } else { + if (typeof column.default === 'string') { + columnToSet.default = `'${escapeSingleQuotes(column.default)}'`; + } else { + if (sqlTypeLowered === 'json') { + columnToSet.default = `'${JSON.stringify(column.default)}'`; + } else if (column.default instanceof Date) { + if (sqlTypeLowered === 'date') { + columnToSet.default = `'${column.default.toISOString().split('T')[0]}'`; + } else if ( + sqlTypeLowered.startsWith('datetime') + || sqlTypeLowered.startsWith('timestamp') + ) { + columnToSet.default = `'${ + column.default + .toISOString() + .replace('T', ' ') + .slice(0, 23) + }'`; + } + } else { + columnToSet.default = column.default; + } + } + if (['blob', 'text', 'json'].includes(column.getSQLType())) { + columnToSet.default = `(${columnToSet.default})`; + } + } + } + columnsObject[name] = columnToSet; + }); + + primaryKeys.map((pk: PrimaryKeyORM) => { + const originalColumnNames = pk.columns.map((c) => c.name); + const columnNames = pk.columns.map((c: any) => getColumnCasing(c, casing)); + + let name = pk.getName(); + if (casing !== undefined) { + for (let i = 0; i < originalColumnNames.length; i++) { + name = name.replace(originalColumnNames[i], columnNames[i]); + } + } + + primaryKeysObject[name] = { + name, + columns: columnNames, + }; + + // all composite pk's should be treated as notNull + for (const column of pk.columns) { + columnsObject[getColumnCasing(column, casing)].notNull = true; + } + }); + + uniqueConstraints?.map((unq) => { + const columnNames = unq.columns.map((c) => getColumnCasing(c, casing)); + + const name = unq.name ?? uniqueKeyName(table, columnNames); + + const existingUnique = uniqueConstraintObject[name]; + if (typeof existingUnique !== 'undefined') { + console.log( + `\n${ + withStyle.errorWarning( + `We\'ve found duplicated unique constraint names in ${ + chalk.underline.blue( + tableName, + ) + } table. \nThe unique constraint ${ + chalk.underline.blue( + name, + ) + } on the ${ + chalk.underline.blue( + columnNames.join(','), + ) + } columns is confilcting with a unique constraint name already defined for ${ + chalk.underline.blue( + existingUnique.columns.join(','), + ) + } columns\n`, + ) + }`, + ); + process.exit(1); + } + + uniqueConstraintObject[name] = { + name: unq.name!, + columns: columnNames, + }; + }); + + const fks: ForeignKey[] = foreignKeys.map((fk) => { + const tableFrom = tableName; + const onDelete = fk.onDelete ?? 'no action'; + const onUpdate = fk.onUpdate ?? 'no action'; + const reference = fk.reference(); + + const referenceFT = reference.foreignTable; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const tableTo = getTableName(referenceFT); + + const originalColumnsFrom = reference.columns.map((it) => it.name); + const columnsFrom = reference.columns.map((it) => getColumnCasing(it, casing)); + const originalColumnsTo = reference.foreignColumns.map((it) => it.name); + const columnsTo = reference.foreignColumns.map((it) => getColumnCasing(it, casing)); + + let name = fk.getName(); + if (casing !== undefined) { + for (let i = 0; i < originalColumnsFrom.length; i++) { + name = name.replace(originalColumnsFrom[i], columnsFrom[i]); + } + for (let i = 0; i < originalColumnsTo.length; i++) { + name = name.replace(originalColumnsTo[i], columnsTo[i]); + } + } + + return { + name, + tableFrom, + tableTo, + columnsFrom, + columnsTo, + onDelete, + onUpdate, + } as ForeignKey; + }); + + fks.forEach((it) => { + foreignKeysObject[it.name] = it; + }); + + indexes.forEach((value) => { + const columns = value.config.columns; + const name = value.config.name; + + let indexColumns = columns.map((it) => { + if (is(it, SQL)) { + const sql = dialect.sqlToQuery(it, 'indexes').sql; + if (typeof internal!.indexes![name] === 'undefined') { + internal!.indexes![name] = { + columns: { + [sql]: { + isExpression: true, + }, + }, + }; + } else { + if (typeof internal!.indexes![name]?.columns[sql] === 'undefined') { + internal!.indexes![name]!.columns[sql] = { + isExpression: true, + }; + } else { + internal!.indexes![name]!.columns[sql]!.isExpression = true; + } + } + return sql; + } else { + return `${getColumnCasing(it, casing)}`; + } + }); + + if (value.config.unique) { + if (typeof uniqueConstraintObject[name] !== 'undefined') { + console.log( + `\n${ + withStyle.errorWarning( + `We\'ve found duplicated unique constraint names in ${ + chalk.underline.blue( + tableName, + ) + } table. \nThe unique index ${ + chalk.underline.blue( + name, + ) + } on the ${ + chalk.underline.blue( + indexColumns.join(','), + ) + } columns is confilcting with a unique constraint name already defined for ${ + chalk.underline.blue( + uniqueConstraintObject[name].columns.join(','), + ) + } columns\n`, + ) + }`, + ); + process.exit(1); + } + } else { + if (typeof foreignKeysObject[name] !== 'undefined') { + console.log( + `\n${ + withStyle.errorWarning( + `In MySQL, when creating a foreign key, an index is automatically generated with the same name as the foreign key constraint.\n\nWe have encountered a collision between the index name on columns ${ + chalk.underline.blue( + indexColumns.join(','), + ) + } and the foreign key on columns ${ + chalk.underline.blue( + foreignKeysObject[name].columnsFrom.join(','), + ) + }. Please change either the index name or the foreign key name. For more information, please refer to https://dev.mysql.com/doc/refman/8.0/en/constraint-foreign-key.html\n + `, + ) + }`, + ); + process.exit(1); + } + } + + indexesObject[name] = { + name, + columns: indexColumns, + isUnique: value.config.unique ?? false, + using: value.config.using, + algorithm: value.config.algorythm, + lock: value.config.lock, + }; + }); + + checks.forEach((check) => { + check; + const checkName = check.name; + if (typeof checksInTable[tableName] !== 'undefined') { + if (checksInTable[tableName].includes(check.name)) { + console.log( + `\n${ + withStyle.errorWarning( + `We\'ve found duplicated check constraint name in ${ + chalk.underline.blue( + tableName, + ) + }. Please rename your check constraint in the ${ + chalk.underline.blue( + tableName, + ) + } table`, + ) + }`, + ); + process.exit(1); + } + checksInTable[tableName].push(checkName); + } else { + checksInTable[tableName] = [check.name]; + } + + checkConstraintObject[checkName] = { + name: checkName, + value: dialect.sqlToQuery(check.value).sql, + }; + }); + + // only handle tables without schemas + if (!schema) { + result[tableName] = { + name: tableName, + columns: columnsObject, + indexes: indexesObject, + foreignKeys: foreignKeysObject, + compositePrimaryKeys: primaryKeysObject, + uniqueConstraints: uniqueConstraintObject, + checkConstraint: checkConstraintObject, + }; + } + } + + for (const view of views) { + const { + isExisting, + name, + query, + schema, + selectedFields, + algorithm, + sqlSecurity, + withCheckOption, + } = getViewConfig(view); + + const columnsObject: Record = {}; + + const existingView = resultViews[name]; + if (typeof existingView !== 'undefined') { + console.log( + `\n${ + withStyle.errorWarning( + `We\'ve found duplicated view name across ${ + chalk.underline.blue( + schema ?? 'public', + ) + } schema. Please rename your view`, + ) + }`, + ); + process.exit(1); + } + + for (const key in selectedFields) { + if (is(selectedFields[key], GoogleSqlColumn)) { + const column = selectedFields[key]; + + const notNull: boolean = column.notNull; + const sqlTypeLowered = column.getSQLType().toLowerCase(); + const autoIncrement = typeof (column as any).autoIncrement === 'undefined' + ? false + : (column as any).autoIncrement; + + const generated = column.generated; + + const columnToSet: Column = { + name: column.name, + type: column.getSQLType(), + primaryKey: false, + // If field is autoincrement it's notNull by default + // notNull: autoIncrement ? true : notNull, + notNull, + autoincrement: autoIncrement, + onUpdate: (column as any).hasOnUpdateNow, + generated: generated + ? { + as: is(generated.as, SQL) + ? dialect.sqlToQuery(generated.as as SQL).sql + : typeof generated.as === 'function' + ? dialect.sqlToQuery(generated.as() as SQL).sql + : (generated.as as any), + type: generated.mode ?? 'stored', + } + : undefined, + }; + + if (column.default !== undefined) { + if (is(column.default, SQL)) { + columnToSet.default = sqlToStr(column.default, casing); + } else { + if (typeof column.default === 'string') { + columnToSet.default = `'${column.default}'`; + } else { + if (sqlTypeLowered === 'json') { + columnToSet.default = `'${JSON.stringify(column.default)}'`; + } else if (column.default instanceof Date) { + if (sqlTypeLowered === 'date') { + columnToSet.default = `'${column.default.toISOString().split('T')[0]}'`; + } else if ( + sqlTypeLowered.startsWith('datetime') + || sqlTypeLowered.startsWith('timestamp') + ) { + columnToSet.default = `'${ + column.default + .toISOString() + .replace('T', ' ') + .slice(0, 23) + }'`; + } + } else { + columnToSet.default = column.default; + } + } + if (['blob', 'text', 'json'].includes(column.getSQLType())) { + columnToSet.default = `(${columnToSet.default})`; + } + } + } + columnsObject[column.name] = columnToSet; + } + } + + resultViews[name] = { + columns: columnsObject, + name, + isExisting, + definition: isExisting ? undefined : dialect.sqlToQuery(query!).sql, + withCheckOption, + algorithm: algorithm ?? 'undefined', // set default values + sqlSecurity: sqlSecurity ?? 'definer', // set default values + }; + } + + return { + version: '5', + dialect: 'googlesql', + tables: result, + views: resultViews, + _meta: { + tables: {}, + columns: {}, + }, + internal, + }; +}; + +function clearDefaults(defaultValue: any, collate: string) { + if (typeof collate === 'undefined' || collate === null) { + collate = `utf8mb4`; + } + + let resultDefault = defaultValue; + collate = `_${collate}`; + if (defaultValue.startsWith(collate)) { + resultDefault = resultDefault + .substring(collate.length, defaultValue.length) + .replace(/\\/g, ''); + if (resultDefault.startsWith("'") && resultDefault.endsWith("'")) { + return `('${escapeSingleQuotes(resultDefault.substring(1, resultDefault.length - 1))}')`; + } else { + return `'${escapeSingleQuotes(resultDefault.substring(1, resultDefault.length - 1))}'`; + } + } else { + return `(${resultDefault})`; + } +} + +export const fromDatabase = async ( + db: DB, + inputSchema: string, + tablesFilter: (table: string) => boolean = (table) => true, + progressCallback?: ( + stage: IntrospectStage, + count: number, + status: IntrospectStatus, + ) => void, +): Promise => { + const result: Record = {}; + const internals: GoogleSqlKitInternals = { tables: {}, indexes: {} }; + + const columns = await db.query(`select * from information_schema.columns + where table_schema = '${inputSchema}' and table_name != '__drizzle_migrations' + order by table_name, ordinal_position;`); + + const response = columns as RowDataPacket[]; + + const schemas: string[] = []; + + let columnsCount = 0; + let tablesCount = new Set(); + let indexesCount = 0; + let foreignKeysCount = 0; + let checksCount = 0; + let viewsCount = 0; + + const idxs = await db.query( + `select * from INFORMATION_SCHEMA.STATISTICS + WHERE INFORMATION_SCHEMA.STATISTICS.TABLE_SCHEMA = '${inputSchema}' and INFORMATION_SCHEMA.STATISTICS.INDEX_NAME != 'PRIMARY';`, + ); + + const idxRows = idxs as RowDataPacket[]; + + for (const column of response) { + if (!tablesFilter(column['TABLE_NAME'] as string)) continue; + + columnsCount += 1; + if (progressCallback) { + progressCallback('columns', columnsCount, 'fetching'); + } + const schema: string = column['TABLE_SCHEMA']; + const tableName = column['TABLE_NAME']; + + tablesCount.add(`${schema}.${tableName}`); + if (progressCallback) { + progressCallback('columns', tablesCount.size, 'fetching'); + } + const columnName: string = column['COLUMN_NAME']; + const isNullable = column['IS_NULLABLE'] === 'YES'; // 'YES', 'NO' + const dataType = column['DATA_TYPE']; // varchar + const columnType = column['COLUMN_TYPE']; // varchar(256) + const isPrimary = column['COLUMN_KEY'] === 'PRI'; // 'PRI', '' + const columnDefault: string = column['COLUMN_DEFAULT']; + const collation: string = column['CHARACTER_SET_NAME']; + const geenratedExpression: string = column['GENERATION_EXPRESSION']; + + let columnExtra = column['EXTRA']; + let isAutoincrement = false; // 'auto_increment', '' + let isDefaultAnExpression = false; // 'auto_increment', '' + + if (typeof column['EXTRA'] !== 'undefined') { + columnExtra = column['EXTRA']; + isAutoincrement = column['EXTRA'] === 'auto_increment'; // 'auto_increment', '' + isDefaultAnExpression = column['EXTRA'].includes('DEFAULT_GENERATED'); // 'auto_increment', '' + } + + // if (isPrimary) { + // if (typeof tableToPk[tableName] === "undefined") { + // tableToPk[tableName] = [columnName]; + // } else { + // tableToPk[tableName].push(columnName); + // } + // } + + if (schema !== inputSchema) { + schemas.push(schema); + } + + const table = result[tableName]; + + // let changedType = columnType.replace("bigint unsigned", "serial") + let changedType = columnType; + + if (columnType === 'bigint unsigned' && !isNullable && isAutoincrement) { + // check unique here + const uniqueIdx = idxRows.filter( + (it) => + it['COLUMN_NAME'] === columnName + && it['TABLE_NAME'] === tableName + && it['NON_UNIQUE'] === 0, + ); + if (uniqueIdx && uniqueIdx.length === 1) { + changedType = columnType.replace('bigint unsigned', 'serial'); + } + } + + if (columnType.includes('decimal(10,0)')) { + changedType = columnType.replace('decimal(10,0)', 'decimal'); + } + + let onUpdate: boolean | undefined = undefined; + if ( + columnType.startsWith('timestamp') + && typeof columnExtra !== 'undefined' + && columnExtra.includes('on update CURRENT_TIMESTAMP') + ) { + onUpdate = true; + } + + const newColumn: Column = { + default: columnDefault === null || columnDefault === undefined + ? undefined + : /^-?[\d.]+(?:e-?\d+)?$/.test(columnDefault) + && !['decimal', 'char', 'varchar'].some((type) => columnType.startsWith(type)) + ? Number(columnDefault) + : isDefaultAnExpression + ? clearDefaults(columnDefault, collation) + : `'${escapeSingleQuotes(columnDefault)}'`, + autoincrement: isAutoincrement, + name: columnName, + type: changedType, + primaryKey: false, + notNull: !isNullable, + onUpdate, + generated: geenratedExpression + ? { + as: geenratedExpression, + type: columnExtra === 'VIRTUAL GENERATED' ? 'virtual' : 'stored', + } + : undefined, + }; + + // Set default to internal object + if (isDefaultAnExpression) { + if (typeof internals!.tables![tableName] === 'undefined') { + internals!.tables![tableName] = { + columns: { + [columnName]: { + isDefaultAnExpression: true, + }, + }, + }; + } else { + if ( + typeof internals!.tables![tableName]!.columns[columnName] + === 'undefined' + ) { + internals!.tables![tableName]!.columns[columnName] = { + isDefaultAnExpression: true, + }; + } else { + internals!.tables![tableName]!.columns[ + columnName + ]!.isDefaultAnExpression = true; + } + } + } + + if (!table) { + result[tableName] = { + name: tableName, + columns: { + [columnName]: newColumn, + }, + compositePrimaryKeys: {}, + indexes: {}, + foreignKeys: {}, + uniqueConstraints: {}, + checkConstraint: {}, + }; + } else { + result[tableName]!.columns[columnName] = newColumn; + } + } + + const tablePks = await db.query( + `SELECT table_name, column_name, ordinal_position + FROM information_schema.table_constraints t + LEFT JOIN information_schema.key_column_usage k + USING(constraint_name,table_schema,table_name) + WHERE t.constraint_type='PRIMARY KEY' + and table_name != '__drizzle_migrations' + AND t.table_schema = '${inputSchema}' + ORDER BY ordinal_position`, + ); + + const tableToPk: { [tname: string]: string[] } = {}; + + const tableToPkRows = tablePks as RowDataPacket[]; + for (const tableToPkRow of tableToPkRows) { + const tableName: string = tableToPkRow['TABLE_NAME']; + const columnName: string = tableToPkRow['COLUMN_NAME']; + const position: string = tableToPkRow['ordinal_position']; + + if (typeof result[tableName] === 'undefined') { + continue; + } + + if (typeof tableToPk[tableName] === 'undefined') { + tableToPk[tableName] = [columnName]; + } else { + tableToPk[tableName].push(columnName); + } + } + + for (const [key, value] of Object.entries(tableToPk)) { + // if (value.length > 1) { + result[key].compositePrimaryKeys = { + [`${key}_${value.join('_')}`]: { + name: `${key}_${value.join('_')}`, + columns: value, + }, + }; + // } else if (value.length === 1) { + // result[key].columns[value[0]].primaryKey = true; + // } else { + // } + } + if (progressCallback) { + progressCallback('columns', columnsCount, 'done'); + progressCallback('tables', tablesCount.size, 'done'); + } + try { + const fks = await db.query( + `SELECT + kcu.TABLE_SCHEMA, + kcu.TABLE_NAME, + kcu.CONSTRAINT_NAME, + kcu.COLUMN_NAME, + kcu.REFERENCED_TABLE_SCHEMA, + kcu.REFERENCED_TABLE_NAME, + kcu.REFERENCED_COLUMN_NAME, + rc.UPDATE_RULE, + rc.DELETE_RULE + FROM + INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu + LEFT JOIN + information_schema.referential_constraints rc + ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME + WHERE kcu.TABLE_SCHEMA = '${inputSchema}' AND kcu.CONSTRAINT_NAME != 'PRIMARY' + AND kcu.REFERENCED_TABLE_NAME IS NOT NULL;`, + ); + + const fkRows = fks as RowDataPacket[]; + + for (const fkRow of fkRows) { + foreignKeysCount += 1; + if (progressCallback) { + progressCallback('fks', foreignKeysCount, 'fetching'); + } + const tableSchema = fkRow['TABLE_SCHEMA']; + const tableName: string = fkRow['TABLE_NAME']; + const constraintName = fkRow['CONSTRAINT_NAME']; + const columnName: string = fkRow['COLUMN_NAME']; + const refTableSchema = fkRow['REFERENCED_TABLE_SCHEMA']; + const refTableName = fkRow['REFERENCED_TABLE_NAME']; + const refColumnName: string = fkRow['REFERENCED_COLUMN_NAME']; + const updateRule: string = fkRow['UPDATE_RULE']; + const deleteRule = fkRow['DELETE_RULE']; + + const tableInResult = result[tableName]; + if (typeof tableInResult === 'undefined') continue; + + if (typeof tableInResult.foreignKeys[constraintName] !== 'undefined') { + tableInResult.foreignKeys[constraintName]!.columnsFrom.push(columnName); + tableInResult.foreignKeys[constraintName]!.columnsTo.push( + refColumnName, + ); + } else { + tableInResult.foreignKeys[constraintName] = { + name: constraintName, + tableFrom: tableName, + tableTo: refTableName, + columnsFrom: [columnName], + columnsTo: [refColumnName], + onDelete: deleteRule?.toLowerCase(), + onUpdate: updateRule?.toLowerCase(), + }; + } + + tableInResult.foreignKeys[constraintName]!.columnsFrom = [ + ...new Set(tableInResult.foreignKeys[constraintName]!.columnsFrom), + ]; + + tableInResult.foreignKeys[constraintName]!.columnsTo = [ + ...new Set(tableInResult.foreignKeys[constraintName]!.columnsTo), + ]; + } + } catch (e) { + // console.log(`Can't proccess foreign keys`); + } + if (progressCallback) { + progressCallback('fks', foreignKeysCount, 'done'); + } + + for (const idxRow of idxRows) { + const tableSchema = idxRow['TABLE_SCHEMA']; + const tableName = idxRow['TABLE_NAME']; + const constraintName = idxRow['INDEX_NAME']; + const columnName: string = idxRow['COLUMN_NAME']; + const isUnique = idxRow['NON_UNIQUE'] === 0; + + const tableInResult = result[tableName]; + if (typeof tableInResult === 'undefined') continue; + + // if (tableInResult.columns[columnName].type === "serial") continue; + + indexesCount += 1; + if (progressCallback) { + progressCallback('indexes', indexesCount, 'fetching'); + } + + if (isUnique) { + if ( + typeof tableInResult.uniqueConstraints[constraintName] !== 'undefined' + ) { + tableInResult.uniqueConstraints[constraintName]!.columns.push( + columnName, + ); + } else { + tableInResult.uniqueConstraints[constraintName] = { + name: constraintName, + columns: [columnName], + }; + } + } else { + // in MySQL FK creates index by default. Name of index is the same as fk constraint name + // so for introspect we will just skip it + if (typeof tableInResult.foreignKeys[constraintName] === 'undefined') { + if (typeof tableInResult.indexes[constraintName] !== 'undefined') { + tableInResult.indexes[constraintName]!.columns.push(columnName); + } else { + tableInResult.indexes[constraintName] = { + name: constraintName, + columns: [columnName], + isUnique: isUnique, + }; + } + } + } + } + + const views = await db.query( + `select * from INFORMATION_SCHEMA.VIEWS WHERE table_schema = '${inputSchema}';`, + ); + + const resultViews: Record = {}; + + viewsCount = views.length; + if (progressCallback) { + progressCallback('views', viewsCount, 'fetching'); + } + for await (const view of views) { + const viewName = view['TABLE_NAME']; + const definition = view['VIEW_DEFINITION']; + + const withCheckOption = view['CHECK_OPTION'] === 'NONE' ? undefined : view['CHECK_OPTION'].toLowerCase(); + const sqlSecurity = view['SECURITY_TYPE'].toLowerCase(); + + const [createSqlStatement] = await db.query(`SHOW CREATE VIEW \`${viewName}\`;`); + const algorithmMatch = createSqlStatement['Create View'].match(/ALGORITHM=([^ ]+)/); + const algorithm = algorithmMatch ? algorithmMatch[1].toLowerCase() : undefined; + + const columns = result[viewName].columns; + delete result[viewName]; + + resultViews[viewName] = { + columns: columns, + isExisting: false, + name: viewName, + algorithm, + definition, + sqlSecurity, + withCheckOption, + }; + } + + if (progressCallback) { + progressCallback('indexes', indexesCount, 'done'); + // progressCallback("enums", 0, "fetching"); + progressCallback('enums', 0, 'done'); + progressCallback('views', viewsCount, 'done'); + } + + const checkConstraints = await db.query( + `SELECT + tc.table_name, + tc.constraint_name, + cc.check_clause +FROM + information_schema.table_constraints tc +JOIN + information_schema.check_constraints cc + ON tc.constraint_name = cc.constraint_name +WHERE + tc.constraint_schema = '${inputSchema}' +AND + tc.constraint_type = 'CHECK';`, + ); + + checksCount += checkConstraints.length; + if (progressCallback) { + progressCallback('checks', checksCount, 'fetching'); + } + for (const checkConstraintRow of checkConstraints) { + const constraintName = checkConstraintRow['CONSTRAINT_NAME']; + const constraintValue = checkConstraintRow['CHECK_CLAUSE']; + const tableName = checkConstraintRow['TABLE_NAME']; + + const tableInResult = result[tableName]; + // if (typeof tableInResult === 'undefined') continue; + + tableInResult.checkConstraint[constraintName] = { + name: constraintName, + value: constraintValue, + }; + } + + if (progressCallback) { + progressCallback('checks', checksCount, 'done'); + } + + return { + version: '5', + dialect: 'googlesql', + tables: result, + views: resultViews, + _meta: { + tables: {}, + columns: {}, + }, + internal: internals, + }; +}; diff --git a/drizzle-kit/src/snapshotsDiffer.ts b/drizzle-kit/src/snapshotsDiffer.ts index 0fd803288a..868b6abbc8 100644 --- a/drizzle-kit/src/snapshotsDiffer.ts +++ b/drizzle-kit/src/snapshotsDiffer.ts @@ -121,6 +121,15 @@ import { prepareSqliteAlterColumns, prepareSQLiteCreateTable, prepareSqliteCreateViewJson, + prepareAddCompositePrimaryKeyGoogleSql, + prepareDeleteCompositePrimaryKeyGoogleSql, + prepareAlterCompositePrimaryKeyGoogleSql, + prepareAlterColumnsGooglesql, + prepareGoogleSqlCreateTableJson, + prepareGoogleSqlCreateViewJson, + JsonCreateGoogleSqlViewStatement, + prepareGoogleSqlAlterView, + JsonAlterGoogleSqlViewStatement, } from './jsonStatements'; import { Named, NamedWithSchema } from './cli/commands/migrate'; @@ -143,6 +152,7 @@ import { SingleStoreSchema, SingleStoreSchemaSquashed, SingleStoreSquasher } fro import { SQLiteSchema, SQLiteSchemaSquashed, SQLiteSquasher, View as SqliteView } from './serializer/sqliteSchema'; import { libSQLCombineStatements, singleStoreCombineStatements, sqliteCombineStatements } from './statementCombiner'; import { copy, prepareMigrationMeta } from './utils'; +import { GoogleSqlSchema, GoogleSqlSchemaSquashed } from './serializer/googlesqlSchema'; const makeChanged = (schema: T) => { return object({ @@ -381,6 +391,17 @@ const alteredMySqlViewSchema = alteredViewCommon.merge( }).strict(), ); +// TODO - SPANNER - verify +const alteredGoogleSqlViewSchema = alteredViewCommon.merge( + object({ + alteredMeta: object({ + __old: string(), + __new: string(), + }).strict().optional(), + }).strict(), +); + + export const diffResultScheme = object({ alteredTablesWithColumns: alteredTableScheme.array(), alteredEnums: changedEnumSchema.array(), @@ -396,6 +417,12 @@ export const diffResultSchemeMysql = object({ alteredViews: alteredMySqlViewSchema.array(), }); +export const diffResultSchemeGooglesql = object({ + alteredTablesWithColumns: alteredTableScheme.array(), + alteredEnums: never().array(), + alteredViews: alteredGoogleSqlViewSchema.array(), +}); + export const diffResultSchemeSingleStore = object({ alteredTablesWithColumns: alteredTableScheme.array(), alteredEnums: never().array(), @@ -415,6 +442,7 @@ export type Table = TypeOf; export type AlteredTable = TypeOf; export type DiffResult = TypeOf; export type DiffResultMysql = TypeOf; +export type DiffResulGooglesql = TypeOf; export type DiffResultSingleStore = TypeOf; export type DiffResultSQLite = TypeOf; @@ -4290,5 +4318,599 @@ export const applyLibSQLSnapshotsDiff = async ( }; }; +// TODO - SPANNER - verify +export const applyGooglesqlSnapshotsDiff = async ( + json1: GoogleSqlSchemaSquashed, + json2: GoogleSqlSchemaSquashed, + tablesResolver: ( + input: ResolverInput, + ) => Promise>, + columnsResolver: ( + input: ColumnsResolverInput, + ) => Promise>, + viewsResolver: ( + input: ResolverInput, + ) => Promise>, + prevFull: GoogleSqlSchema, + curFull: GoogleSqlSchema, + action?: 'push' | undefined, +): Promise<{ + statements: JsonStatement[]; + sqlStatements: string[]; + _meta: + | { + schemas: {}; + tables: {}; + columns: {}; + } + | undefined; +}> => { + // squash indexes and fks + + // squash uniqueIndexes and uniqueConstraint into constraints object + // it should be done for mysql only because it has no diffs for it + + // TODO: @AndriiSherman + // Add an upgrade to v6 and move all snaphosts to this strcutre + // After that we can generate mysql in 1 object directly(same as sqlite) + for (const tableName in json1.tables) { + const table = json1.tables[tableName]; + for (const indexName in table.indexes) { + const index = MySqlSquasher.unsquashIdx(table.indexes[indexName]); + if (index.isUnique) { + table.uniqueConstraints[indexName] = MySqlSquasher.squashUnique({ + name: index.name, + columns: index.columns, + }); + delete json1.tables[tableName].indexes[index.name]; + } + } + } + + for (const tableName in json2.tables) { + const table = json2.tables[tableName]; + for (const indexName in table.indexes) { + const index = MySqlSquasher.unsquashIdx(table.indexes[indexName]); + if (index.isUnique) { + table.uniqueConstraints[indexName] = MySqlSquasher.squashUnique({ + name: index.name, + columns: index.columns, + }); + delete json2.tables[tableName].indexes[index.name]; + } + } + } + + const tablesDiff = diffSchemasOrTables(json1.tables, json2.tables); + + const { + created: createdTables, + deleted: deletedTables, + renamed: renamedTables, // renamed or moved + } = await tablesResolver({ + created: tablesDiff.added, + deleted: tablesDiff.deleted, + }); + + const tablesPatchedSnap1 = copy(json1); + tablesPatchedSnap1.tables = mapEntries(tablesPatchedSnap1.tables, (_, it) => { + const { name } = nameChangeFor(it, renamedTables); + it.name = name; + return [name, it]; + }); + + const res = diffColumns(tablesPatchedSnap1.tables, json2.tables); + const columnRenames = [] as { + table: string; + renames: { from: Column; to: Column }[]; + }[]; + + const columnCreates = [] as { + table: string; + columns: Column[]; + }[]; + + const columnDeletes = [] as { + table: string; + columns: Column[]; + }[]; + + for (let entry of Object.values(res)) { + const { renamed, created, deleted } = await columnsResolver({ + tableName: entry.name, + schema: entry.schema, + deleted: entry.columns.deleted, + created: entry.columns.added, + }); + + if (created.length > 0) { + columnCreates.push({ + table: entry.name, + columns: created, + }); + } + + if (deleted.length > 0) { + columnDeletes.push({ + table: entry.name, + columns: deleted, + }); + } + + if (renamed.length > 0) { + columnRenames.push({ + table: entry.name, + renames: renamed, + }); + } + } + + const columnRenamesDict = columnRenames.reduce( + (acc, it) => { + acc[it.table] = it.renames; + return acc; + }, + {} as Record< + string, + { + from: Named; + to: Named; + }[] + >, + ); + + const columnsPatchedSnap1 = copy(tablesPatchedSnap1); + columnsPatchedSnap1.tables = mapEntries( + columnsPatchedSnap1.tables, + (tableKey, tableValue) => { + const patchedColumns = mapKeys( + tableValue.columns, + (columnKey, column) => { + const rens = columnRenamesDict[tableValue.name] || []; + const newName = columnChangeFor(columnKey, rens); + column.name = newName; + return newName; + }, + ); + + tableValue.columns = patchedColumns; + return [tableKey, tableValue]; + }, + ); + + const viewsDiff = diffSchemasOrTables(json1.views, json2.views); + + const { + created: createdViews, + deleted: deletedViews, + renamed: renamedViews, // renamed or moved + } = await viewsResolver({ + created: viewsDiff.added, + deleted: viewsDiff.deleted, + }); + + const renamesViewDic: Record = {}; + renamedViews.forEach((it) => { + renamesViewDic[it.from.name] = { to: it.to.name, from: it.from.name }; + }); + + const viewsPatchedSnap1 = copy(columnsPatchedSnap1); + viewsPatchedSnap1.views = mapEntries( + viewsPatchedSnap1.views, + (viewKey, viewValue) => { + const rename = renamesViewDic[viewValue.name]; + + if (rename) { + viewValue.name = rename.to; + viewKey = rename.to; + } + + return [viewKey, viewValue]; + }, + ); + + const diffResult = applyJsonDiff(viewsPatchedSnap1, json2); + + const typedResult: DiffResulGooglesql = diffResultSchemeGooglesql.parse(diffResult); + + const jsonStatements: JsonStatement[] = []; + + const jsonCreateIndexesForCreatedTables = createdTables + .map((it) => { + return prepareCreateIndexesJson( + it.name, + it.schema, + it.indexes, + curFull.internal, + ); + }) + .flat(); + + const jsonDropTables = deletedTables.map((it) => { + return prepareDropTableJson(it); + }); + + const jsonRenameTables = renamedTables.map((it) => { + return prepareRenameTableJson(it.from, it.to); + }); + + const alteredTables = typedResult.alteredTablesWithColumns; + + const jsonAddedCompositePKs: JsonCreateCompositePK[] = []; + const jsonDeletedCompositePKs: JsonDeleteCompositePK[] = []; + const jsonAlteredCompositePKs: JsonAlterCompositePK[] = []; + + const jsonAddedUniqueConstraints: JsonCreateUniqueConstraint[] = []; + const jsonDeletedUniqueConstraints: JsonDeleteUniqueConstraint[] = []; + const jsonAlteredUniqueConstraints: JsonAlterUniqueConstraint[] = []; + + const jsonCreatedCheckConstraints: JsonCreateCheckConstraint[] = []; + const jsonDeletedCheckConstraints: JsonDeleteCheckConstraint[] = []; + + const jsonRenameColumnsStatements: JsonRenameColumnStatement[] = columnRenames + .map((it) => prepareRenameColumns(it.table, '', it.renames)) + .flat(); + + const jsonAddColumnsStatemets: JsonAddColumnStatement[] = columnCreates + .map((it) => _prepareAddColumns(it.table, '', it.columns)) + .flat(); + + const jsonDropColumnsStatemets: JsonDropColumnStatement[] = columnDeletes + .map((it) => _prepareDropColumns(it.table, '', it.columns)) + .flat(); + + alteredTables.forEach((it) => { + // This part is needed to make sure that same columns in a table are not triggered for change + // there is a case where orm and kit are responsible for pk name generation and one of them is not sorting name + // We double-check that pk with same set of columns are both in added and deleted diffs + let addedColumns: string[] = []; + for (const addedPkName of Object.keys(it.addedCompositePKs)) { + const addedPkColumns = it.addedCompositePKs[addedPkName]; + addedColumns = MySqlSquasher.unsquashPK(addedPkColumns).columns; + } + + let deletedColumns: string[] = []; + for (const deletedPkName of Object.keys(it.deletedCompositePKs)) { + const deletedPkColumns = it.deletedCompositePKs[deletedPkName]; + deletedColumns = MySqlSquasher.unsquashPK(deletedPkColumns).columns; + } + + // Don't need to sort, but need to add tests for it + // addedColumns.sort(); + // deletedColumns.sort(); + const doPerformDeleteAndCreate = JSON.stringify(addedColumns) !== JSON.stringify(deletedColumns); + + let addedCompositePKs: JsonCreateCompositePK[] = []; + let deletedCompositePKs: JsonDeleteCompositePK[] = []; + let alteredCompositePKs: JsonAlterCompositePK[] = []; + + addedCompositePKs = prepareAddCompositePrimaryKeyGoogleSql( + it.name, + it.addedCompositePKs, + prevFull, + curFull, + ); + deletedCompositePKs = prepareDeleteCompositePrimaryKeyGoogleSql( + it.name, + it.deletedCompositePKs, + prevFull, + ); + // } + alteredCompositePKs = prepareAlterCompositePrimaryKeyGoogleSql( + it.name, + it.alteredCompositePKs, + prevFull, + curFull, + ); + + // add logic for unique constraints + let addedUniqueConstraints: JsonCreateUniqueConstraint[] = []; + let deletedUniqueConstraints: JsonDeleteUniqueConstraint[] = []; + let alteredUniqueConstraints: JsonAlterUniqueConstraint[] = []; + + let createdCheckConstraints: JsonCreateCheckConstraint[] = []; + let deletedCheckConstraints: JsonDeleteCheckConstraint[] = []; + + addedUniqueConstraints = prepareAddUniqueConstraint( + it.name, + it.schema, + it.addedUniqueConstraints, + ); + deletedUniqueConstraints = prepareDeleteUniqueConstraint( + it.name, + it.schema, + it.deletedUniqueConstraints, + ); + if (it.alteredUniqueConstraints) { + const added: Record = {}; + const deleted: Record = {}; + for (const k of Object.keys(it.alteredUniqueConstraints)) { + added[k] = it.alteredUniqueConstraints[k].__new; + deleted[k] = it.alteredUniqueConstraints[k].__old; + } + addedUniqueConstraints.push( + ...prepareAddUniqueConstraint(it.name, it.schema, added), + ); + deletedUniqueConstraints.push( + ...prepareDeleteUniqueConstraint(it.name, it.schema, deleted), + ); + } + + createdCheckConstraints = prepareAddCheckConstraint(it.name, it.schema, it.addedCheckConstraints); + deletedCheckConstraints = prepareDeleteCheckConstraint( + it.name, + it.schema, + it.deletedCheckConstraints, + ); + + // skip for push + if (it.alteredCheckConstraints && action !== 'push') { + const added: Record = {}; + const deleted: Record = {}; + + for (const k of Object.keys(it.alteredCheckConstraints)) { + added[k] = it.alteredCheckConstraints[k].__new; + deleted[k] = it.alteredCheckConstraints[k].__old; + } + createdCheckConstraints.push(...prepareAddCheckConstraint(it.name, it.schema, added)); + deletedCheckConstraints.push(...prepareDeleteCheckConstraint(it.name, it.schema, deleted)); + } + + jsonAddedCompositePKs.push(...addedCompositePKs); + jsonDeletedCompositePKs.push(...deletedCompositePKs); + jsonAlteredCompositePKs.push(...alteredCompositePKs); + + jsonAddedUniqueConstraints.push(...addedUniqueConstraints); + jsonDeletedUniqueConstraints.push(...deletedUniqueConstraints); + jsonAlteredUniqueConstraints.push(...alteredUniqueConstraints); + + jsonCreatedCheckConstraints.push(...createdCheckConstraints); + jsonDeletedCheckConstraints.push(...deletedCheckConstraints); + }); + + const rColumns = jsonRenameColumnsStatements.map((it) => { + const tableName = it.tableName; + const schema = it.schema; + return { + from: { schema, table: tableName, column: it.oldColumnName }, + to: { schema, table: tableName, column: it.newColumnName }, + }; + }); + + const jsonTableAlternations = alteredTables + .map((it) => { + return prepareAlterColumnsGooglesql( + it.name, + it.schema, + it.altered, + json1, + json2, + action, + ); + }) + .flat(); + + const jsonCreateIndexesForAllAlteredTables = alteredTables + .map((it) => { + return prepareCreateIndexesJson( + it.name, + it.schema, + it.addedIndexes || {}, + curFull.internal, + ); + }) + .flat(); + + const jsonDropIndexesForAllAlteredTables = alteredTables + .map((it) => { + return prepareDropIndexesJson( + it.name, + it.schema, + it.deletedIndexes || {}, + ); + }) + .flat(); + + alteredTables.forEach((it) => { + const droppedIndexes = Object.keys(it.alteredIndexes).reduce( + (current, item: string) => { + current[item] = it.alteredIndexes[item].__old; + return current; + }, + {} as Record, + ); + const createdIndexes = Object.keys(it.alteredIndexes).reduce( + (current, item: string) => { + current[item] = it.alteredIndexes[item].__new; + return current; + }, + {} as Record, + ); + + jsonCreateIndexesForAllAlteredTables.push( + ...prepareCreateIndexesJson(it.name, it.schema, createdIndexes || {}), + ); + jsonDropIndexesForAllAlteredTables.push( + ...prepareDropIndexesJson(it.name, it.schema, droppedIndexes || {}), + ); + }); + + const jsonCreateReferencesForCreatedTables: JsonCreateReferenceStatement[] = createdTables + .map((it) => { + return prepareCreateReferencesJson(it.name, it.schema, it.foreignKeys); + }) + .flat(); + + const jsonReferencesForAllAlteredTables: JsonReferenceStatement[] = alteredTables + .map((it) => { + const forAdded = prepareCreateReferencesJson( + it.name, + it.schema, + it.addedForeignKeys, + ); + + const forAltered = prepareDropReferencesJson( + it.name, + it.schema, + it.deletedForeignKeys, + ); + + const alteredFKs = prepareAlterReferencesJson( + it.name, + it.schema, + it.alteredForeignKeys, + ); + + return [...forAdded, ...forAltered, ...alteredFKs]; + }) + .flat(); + + const jsonCreatedReferencesForAlteredTables = jsonReferencesForAllAlteredTables.filter( + (t) => t.type === 'create_reference', + ); + const jsonDroppedReferencesForAlteredTables = jsonReferencesForAllAlteredTables.filter( + (t) => t.type === 'delete_reference', + ); + + const jsonGoogleSqlCreateTables = createdTables.map((it) => { + return prepareGoogleSqlCreateTableJson( + it, + curFull as GoogleSqlSchema, + curFull.internal, + ); + }); + + const createViews: JsonCreateGoogleSqlViewStatement[] = []; + const dropViews: JsonDropViewStatement[] = []; + const renameViews: JsonRenameViewStatement[] = []; + const alterViews: JsonAlterGoogleSqlViewStatement[] = []; + + createViews.push( + ...createdViews.filter((it) => !it.isExisting).map((it) => { + return prepareGoogleSqlCreateViewJson( + it.name, + it.definition!, + it.meta, + ); + }), + ); + + dropViews.push( + ...deletedViews.filter((it) => !it.isExisting).map((it) => { + return prepareDropViewJson(it.name); + }), + ); + + renameViews.push( + ...renamedViews.filter((it) => !it.to.isExisting && !json1.views[it.from.name].isExisting).map((it) => { + return prepareRenameViewJson(it.to.name, it.from.name); + }), + ); + + const alteredViews = typedResult.alteredViews.filter((it) => !json2.views[it.name].isExisting); + + for (const alteredView of alteredViews) { + const { definition, meta } = json2.views[alteredView.name]; + + if (alteredView.alteredExisting) { + dropViews.push(prepareDropViewJson(alteredView.name)); + + createViews.push( + prepareGoogleSqlCreateViewJson( + alteredView.name, + definition!, + meta, + ), + ); + + continue; + } + + if (alteredView.alteredDefinition && action !== 'push') { + createViews.push( + prepareGoogleSqlCreateViewJson( + alteredView.name, + definition!, + meta, + true, + ), + ); + continue; + } + + if (alteredView.alteredMeta) { + const view = curFull['views'][alteredView.name]; + alterViews.push( + prepareGoogleSqlAlterView(view), + ); + } + } + + jsonStatements.push(...jsonGoogleSqlCreateTables); + + jsonStatements.push(...jsonDropTables); + jsonStatements.push(...jsonRenameTables); + jsonStatements.push(...jsonRenameColumnsStatements); + + jsonStatements.push(...dropViews); + jsonStatements.push(...renameViews); + jsonStatements.push(...alterViews); + + jsonStatements.push(...jsonDeletedUniqueConstraints); + jsonStatements.push(...jsonDeletedCheckConstraints); + + jsonStatements.push(...jsonDroppedReferencesForAlteredTables); + + // Will need to drop indexes before changing any columns in table + // Then should go column alternations and then index creation + jsonStatements.push(...jsonDropIndexesForAllAlteredTables); + + jsonStatements.push(...jsonDeletedCompositePKs); + jsonStatements.push(...jsonTableAlternations); + jsonStatements.push(...jsonAddedCompositePKs); + jsonStatements.push(...jsonAddColumnsStatemets); + + jsonStatements.push(...jsonAddedUniqueConstraints); + jsonStatements.push(...jsonDeletedUniqueConstraints); + + jsonStatements.push(...jsonCreateReferencesForCreatedTables); + jsonStatements.push(...jsonCreateIndexesForCreatedTables); + jsonStatements.push(...jsonCreatedCheckConstraints); + + jsonStatements.push(...jsonCreatedReferencesForAlteredTables); + jsonStatements.push(...jsonCreateIndexesForAllAlteredTables); + + jsonStatements.push(...jsonDropColumnsStatemets); + + // jsonStatements.push(...jsonDeletedCompositePKs); + // jsonStatements.push(...jsonAddedCompositePKs); + jsonStatements.push(...jsonAlteredCompositePKs); + + jsonStatements.push(...createViews); + + jsonStatements.push(...jsonAlteredUniqueConstraints); + + const sqlStatements = fromJson(jsonStatements, 'googlesql'); + + const uniqueSqlStatements: string[] = []; + sqlStatements.forEach((ss) => { + if (!uniqueSqlStatements.includes(ss)) { + uniqueSqlStatements.push(ss); + } + }); + + const rTables = renamedTables.map((it) => { + return { from: it.from, to: it.to }; + }); + + const _meta = prepareMigrationMeta([], rTables, rColumns); + + return { + statements: jsonStatements, + sqlStatements: uniqueSqlStatements, + _meta, + }; +}; + // explicitely ask if tables were renamed, if yes - add those to altered tables, otherwise - deleted // double check if user wants to delete particular table and warn him on data loss diff --git a/drizzle-kit/src/sqlgenerator.ts b/drizzle-kit/src/sqlgenerator.ts index 491c1ba517..f41dbbdd08 100644 --- a/drizzle-kit/src/sqlgenerator.ts +++ b/drizzle-kit/src/sqlgenerator.ts @@ -20,6 +20,7 @@ import { JsonAlterColumnSetPrimaryKeyStatement, JsonAlterColumnTypeStatement, JsonAlterCompositePK, + JsonAlterGoogleSqlViewStatement, JsonAlterIndPolicyStatement, JsonAlterMySqlViewStatement, JsonAlterPolicyStatement, @@ -37,6 +38,7 @@ import { JsonCreateCheckConstraint, JsonCreateCompositePK, JsonCreateEnumStatement, + JsonCreateGoogleSqlViewStatement, JsonCreateIndexStatement, JsonCreateIndPolicyStatement, JsonCreateMySqlViewStatement, @@ -84,6 +86,7 @@ import { JsonStatement, } from './jsonStatements'; import { Dialect } from './schemaValidator'; +import { GoogleSqlSquasher } from './serializer/googlesqlSchema'; import { MySqlSquasher } from './serializer/mysqlSchema'; import { PgSquasher, policy } from './serializer/pgSchema'; import { SingleStoreSquasher } from './serializer/singlestoreSchema'; @@ -575,6 +578,94 @@ class MySqlCreateTableConvertor extends Convertor { return statement; } } + +// TODO: SPANNER - verify +class GoogleSqlCreateTableConvertor extends Convertor { + can(statement: JsonStatement, dialect: Dialect): boolean { + return statement.type === 'create_table' && dialect === 'googlesql'; + } + + convert(st: JsonCreateTableStatement) { + const { + tableName, + columns, + schema, + checkConstraints, + compositePKs, + uniqueConstraints, + internals, + } = st; + + let statement = ''; + statement += `CREATE TABLE \`${tableName}\` (\n`; + for (let i = 0; i < columns.length; i++) { + const column = columns[i]; + + const primaryKeyStatement = column.primaryKey ? ' PRIMARY KEY' : ''; + const notNullStatement = column.notNull ? ' NOT NULL' : ''; + const defaultStatement = column.default !== undefined ? ` DEFAULT ${column.default}` : ''; + + const onUpdateStatement = column.onUpdate + ? ` ON UPDATE CURRENT_TIMESTAMP` + : ''; + + const autoincrementStatement = column.autoincrement + ? ' AUTO_INCREMENT' + : ''; + + const generatedStatement = column.generated + ? ` GENERATED ALWAYS AS (${column.generated?.as}) ${column.generated?.type.toUpperCase()}` + : ''; + + statement += '\t' + + `\`${column.name}\` ${column.type}${autoincrementStatement}${primaryKeyStatement}${generatedStatement}${notNullStatement}${defaultStatement}${onUpdateStatement}`; + 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})`; + } + } + + if (typeof checkConstraints !== 'undefined' && checkConstraints.length > 0) { + for (const checkConstraint of checkConstraints) { + statement += ',\n'; + const unsquashedCheck = GoogleSqlSquasher.unsquashCheck(checkConstraint); + + statement += `\tCONSTRAINT \`${unsquashedCheck.name}\` CHECK(${unsquashedCheck.value})`; + } + } + + statement += `\n);`; + statement += `\n`; + return statement; + } +} + export class SingleStoreCreateTableConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'create_table' && dialect === 'singlestore'; @@ -808,6 +899,28 @@ 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; + + let statement = `CREATE `; + statement += replace ? `OR REPLACE ` : ''; + statement += algorithm ? `ALGORITHM = ${algorithm}\n` : ''; + statement += sqlSecurity ? `SQL SECURITY ${sqlSecurity}\n` : ''; + statement += `VIEW \`${name}\` AS (${definition})`; + statement += withCheckOption ? `\nWITH ${withCheckOption} CHECK OPTION` : ''; + + statement += ';'; + + return statement; + } +} + class SqliteCreateViewConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'sqlite_create_view' && (dialect === 'sqlite' || dialect === 'turso'); @@ -846,6 +959,19 @@ class MySqlDropViewConvertor extends Convertor { } } +// TODO: SPANNER - verify +class GoogleSqlDropViewConvertor extends Convertor { + can(statement: JsonStatement, dialect: Dialect): boolean { + return statement.type === 'drop_view' && dialect === 'googlesql'; + } + + convert(st: JsonDropViewStatement) { + const { name } = st; + + return `DROP VIEW \`${name}\`;`; + } +} + class SqliteDropViewConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'drop_view' && (dialect === 'sqlite' || dialect === 'turso'); @@ -878,6 +1004,26 @@ class MySqlAlterViewConvertor extends Convertor { } } +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; + + let statement = `ALTER `; + statement += algorithm ? `ALGORITHM = ${algorithm}\n` : ''; + statement += sqlSecurity ? `SQL SECURITY ${sqlSecurity}\n` : ''; + statement += `VIEW \`${name}\` AS ${definition}`; + statement += withCheckOption ? `\nWITH ${withCheckOption} CHECK OPTION` : ''; + + statement += ';'; + + return statement; + } +} + class PgRenameViewConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'rename_view' && dialect === 'postgresql'; @@ -904,6 +1050,19 @@ 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}\`;`; + } +} + class PgAlterViewSchemaConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'alter_view_alter_schema' && dialect === 'postgresql'; @@ -1218,6 +1377,20 @@ class MySQLAlterTableAddUniqueConstraintConvertor extends Convertor { } } +// TODO: SPANNER - verify +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('`,`') + }\`);`; + } +} + class MySQLAlterTableDropUniqueConstraintConvertor extends Convertor { can(statement: JsonDeleteUniqueConstraint, dialect: Dialect): boolean { return statement.type === 'delete_unique_constraint' && dialect === 'mysql'; @@ -1229,6 +1402,19 @@ class MySQLAlterTableDropUniqueConstraintConvertor extends Convertor { } } +// TODO: SPANNER - verify +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}\`;`; + } +} + + class MySqlAlterTableAddCheckConstraintConvertor extends Convertor { can(statement: JsonCreateCheckConstraint, dialect: Dialect): boolean { return ( @@ -1243,6 +1429,21 @@ class MySqlAlterTableAddCheckConstraintConvertor extends Convertor { } } +// TODO: SPANNER - verify +class GoogleSqlAlterTableAddCheckConstraintConvertor extends Convertor { + can(statement: JsonCreateCheckConstraint, dialect: Dialect): boolean { + return ( + statement.type === 'create_check_constraint' && dialect === 'googlesql' + ); + } + convert(statement: JsonCreateCheckConstraint): string { + const unsquashed = GoogleSqlSquasher.unsquashCheck(statement.data); + const { tableName } = statement; + + return `ALTER TABLE \`${tableName}\` ADD CONSTRAINT \`${unsquashed.name}\` CHECK (${unsquashed.value});`; + } +} + class SingleStoreAlterTableAddUniqueConstraintConvertor extends Convertor { can(statement: JsonCreateUniqueConstraint, dialect: Dialect): boolean { return statement.type === 'create_unique_constraint' && dialect === 'singlestore'; @@ -1279,6 +1480,20 @@ class MySqlAlterTableDeleteCheckConstraintConvertor extends Convertor { } } +// TODO: SPANNER - verify +class GoogleSqlAlterTableDeleteCheckConstraintConvertor extends Convertor { + can(statement: JsonDeleteCheckConstraint, dialect: Dialect): boolean { + return ( + statement.type === 'delete_check_constraint' && dialect === 'googlesql' + ); + } + convert(statement: JsonDeleteCheckConstraint): string { + const { tableName } = statement; + + return `ALTER TABLE \`${tableName}\` DROP CONSTRAINT \`${statement.constraintName}\`;`; + } +} + class CreatePgSequenceConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'create_sequence' && dialect === 'postgresql'; @@ -1532,6 +1747,18 @@ class MySQLDropTableConvertor extends Convertor { } } +// TODO: SPANNER - verify +class GoogleSQLDropTableConvertor extends Convertor { + can(statement: JsonStatement, dialect: Dialect): boolean { + return statement.type === 'drop_table' && dialect === 'googlesql'; + } + + convert(statement: JsonDropTableStatement) { + const { tableName } = statement; + return `DROP TABLE \`${tableName}\`;`; + } +} + export class SingleStoreDropTableConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'drop_table' && dialect === 'singlestore'; @@ -1591,6 +1818,18 @@ class MySqlRenameTableConvertor extends Convertor { } } +// TODO: SPANNER - verify +class GoogleSqlRenameTableConvertor extends Convertor { + can(statement: JsonStatement, dialect: Dialect): boolean { + return statement.type === 'rename_table' && dialect === 'googlesql'; + } + + convert(statement: JsonRenameTableStatement) { + const { tableNameFrom, tableNameTo } = statement; + return `RENAME TABLE \`${tableNameFrom}\` TO \`${tableNameTo}\`;`; + } +} + export class SingleStoreRenameTableConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'rename_table' && dialect === 'singlestore'; @@ -1633,6 +1872,20 @@ class MySqlAlterTableRenameColumnConvertor extends Convertor { } } +// TODO: SPANNER - verify +class GoogleSqlAlterTableRenameColumnConvertor extends Convertor { + can(statement: JsonStatement, dialect: Dialect): boolean { + return ( + statement.type === 'alter_table_rename_column' && dialect === 'googlesql' + ); + } + + convert(statement: JsonRenameColumnStatement) { + const { tableName, oldColumnName, newColumnName } = statement; + return `ALTER TABLE \`${tableName}\` RENAME COLUMN \`${oldColumnName}\` TO \`${newColumnName}\`;`; + } +} + class SingleStoreAlterTableRenameColumnConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return ( @@ -1688,6 +1941,18 @@ 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'; + } + + convert(statement: JsonDropColumnStatement) { + const { tableName, columnName } = statement; + return `ALTER TABLE \`${tableName}\` DROP COLUMN \`${columnName}\`;`; + } +} + class SingleStoreAlterTableDropColumnConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'alter_table_drop_column' && dialect === 'singlestore'; @@ -1806,6 +2071,38 @@ 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'; + } + + convert(statement: JsonAddColumnStatement) { + const { tableName, column } = statement; + const { + 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()}` + : ''; + + return `ALTER TABLE \`${tableName}\` ADD \`${name}\` ${type}${primaryKeyStatement}${autoincrementStatement}${defaultStatement}${generatedStatement}${notNullStatement}${onUpdateStatement};`; + } +} + class SingleStoreAlterTableAddColumnConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'alter_table_add_column' && dialect === 'singlestore'; @@ -2238,21 +2535,70 @@ class MySqlAlterTableAlterColumnAlterrGeneratedConvertor extends Convertor { } } -class MySqlAlterTableAlterColumnSetDefaultConvertor extends Convertor { +// TODO: SPANNER - verify +class GoogleSqlAlterTableAlterColumnAlterGeneratedConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return ( - statement.type === 'alter_table_alter_column_set_default' - && dialect === 'mysql' + statement.type === 'alter_table_alter_column_alter_generated' + && dialect === 'googlesql' ); } - convert(statement: JsonAlterColumnSetDefaultStatement) { - const { tableName, columnName } = statement; - return `ALTER TABLE \`${tableName}\` ALTER COLUMN \`${columnName}\` SET DEFAULT ${statement.newDefaultValue};`; - } -} - -class MySqlAlterTableAlterColumnDropDefaultConvertor extends Convertor { + convert(statement: JsonAlterColumnAlterGeneratedStatement) { + const { + tableName, + columnName, + schema, + columnNotNull: notNull, + columnDefault, + columnOnUpdate, + columnAutoIncrement, + columnPk, + columnGenerated, + } = statement; + + 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', + }); + + return [ + `ALTER TABLE ${tableNameWithSchema} drop column \`${columnName}\`;`, + addColumnStatement, + ]; + } +} + +class MySqlAlterTableAlterColumnSetDefaultConvertor extends Convertor { + can(statement: JsonStatement, dialect: Dialect): boolean { + return ( + statement.type === 'alter_table_alter_column_set_default' + && dialect === 'mysql' + ); + } + + convert(statement: JsonAlterColumnSetDefaultStatement) { + const { tableName, columnName } = statement; + return `ALTER TABLE \`${tableName}\` ALTER COLUMN \`${columnName}\` SET DEFAULT ${statement.newDefaultValue};`; + } +} + +class MySqlAlterTableAlterColumnDropDefaultConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return ( statement.type === 'alter_table_alter_column_drop_default' @@ -2278,6 +2624,19 @@ class MySqlAlterTableAddPk extends Convertor { } } +// TODO: SPANNER - verify +class GoogleSqlAlterTableAddPk extends Convertor { + can(statement: JsonStatement, dialect: string): boolean { + return ( + statement.type === 'alter_table_alter_column_set_pk' + && dialect === 'googlesql' + ); + } + convert(statement: JsonAlterColumnSetPrimaryKeyStatement): string { + return `ALTER TABLE \`${statement.tableName}\` ADD PRIMARY KEY (\`${statement.columnName}\`);`; + } +} + class MySqlAlterTableDropPk extends Convertor { can(statement: JsonStatement, dialect: string): boolean { return ( @@ -2290,6 +2649,19 @@ class MySqlAlterTableDropPk extends Convertor { } } +// TODO: SPANNER - verify +class GoogleSqlAlterTableDropPk extends Convertor { + can(statement: JsonStatement, dialect: string): boolean { + return ( + statement.type === 'alter_table_alter_column_drop_pk' + && dialect === 'googlesql' + ); + } + convert(statement: JsonAlterColumnDropPrimaryKeyStatement): string { + return `ALTER TABLE \`${statement.tableName}\` DROP PRIMARY KEY`; + } +} + type LibSQLModifyColumnStatement = | JsonAlterColumnTypeStatement | JsonAlterColumnDropNotNullStatement @@ -2640,6 +3012,244 @@ class MySqlModifyColumn extends Convertor { } } +type GoogleSqlModifyColumnStatement = + | JsonAlterColumnDropNotNullStatement + | JsonAlterColumnSetNotNullStatement + | JsonAlterColumnTypeStatement + | JsonAlterColumnDropOnUpdateStatement + | JsonAlterColumnSetOnUpdateStatement + | JsonAlterColumnDropAutoincrementStatement + | JsonAlterColumnSetAutoincrementStatement + | JsonAlterColumnSetDefaultStatement + | JsonAlterColumnDropDefaultStatement + | 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 ( + (statement.type === 'alter_table_alter_column_set_type' + || statement.type === 'alter_table_alter_column_set_notnull' + || statement.type === 'alter_table_alter_column_drop_notnull' + || statement.type === 'alter_table_alter_column_drop_on_update' + || statement.type === 'alter_table_alter_column_set_on_update' + || statement.type === 'alter_table_alter_column_set_autoincrement' + || statement.type === 'alter_table_alter_column_drop_autoincrement' + || statement.type === 'alter_table_alter_column_set_default' + || statement.type === 'alter_table_alter_column_drop_default' + || statement.type === 'alter_table_alter_column_set_generated' + || statement.type === 'alter_table_alter_column_drop_generated') + && dialect === 'googlesql' + ); + } + + convert(statement: GoogleSqlModifyColumnStatement) { + const { tableName, columnName } = statement; + let columnType = ``; + let columnDefault: any = ''; + let columnNotNull = ''; + let columnOnUpdate = ''; + let columnAutoincrement = ''; + let primaryKey = statement.columnPk ? ' PRIMARY KEY' : ''; + let columnGenerated = ''; + + if (statement.type === 'alter_table_alter_column_drop_notnull') { + columnType = ` ${statement.newDataType}`; + columnDefault = statement.columnDefault + ? ` 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' + : ''; + } 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' + : ''; + } 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'; + } 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 = ''; + } 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' + : ''; + } 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' + : ''; + } 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' + : ''; + + 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' + : ''; + + if (statement.oldColumn?.generated?.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 { + 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' + : ''; + columnGenerated = statement.columnGenerated + ? ` GENERATED ALWAYS AS (${statement.columnGenerated?.as}) ${statement.columnGenerated?.type.toUpperCase()}` + : ''; + } + + // Seems like getting value from simple json2 shanpshot makes dates be dates + columnDefault = columnDefault instanceof Date + ? columnDefault.toISOString() + : columnDefault; + + return `ALTER TABLE \`${tableName}\` MODIFY COLUMN \`${columnName}\`${columnType}${columnAutoincrement}${columnGenerated}${columnNotNull}${columnDefault}${columnOnUpdate};`; + } +} + + class SingleStoreAlterTableAlterColumnAlterrGeneratedConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return ( @@ -3059,6 +3669,18 @@ class MySqlAlterTableCreateCompositePrimaryKeyConvertor extends Convertor { } } +// TODO: SPANNER - verify +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('`,`')}\`);`; + } +} + class MySqlAlterTableDeleteCompositePrimaryKeyConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'delete_composite_pk' && dialect === 'mysql'; @@ -3070,6 +3692,17 @@ class MySqlAlterTableDeleteCompositePrimaryKeyConvertor extends Convertor { } } +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;`; + } +} + class MySqlAlterTableAlterCompositePrimaryKeyConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'alter_composite_pk' && dialect === 'mysql'; @@ -3084,6 +3717,21 @@ class MySqlAlterTableAlterCompositePrimaryKeyConvertor extends Convertor { } } +// TODO: SPANNER - verify +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('`,`')}\`);`; + } +} + class SqliteAlterTableCreateCompositePrimaryKeyConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'create_composite_pk' && dialect === 'sqlite'; @@ -3347,6 +3995,31 @@ class MySqlCreateForeignKeyConvertor extends Convertor { } } +// TODO: SPANNER - verify +class GoogleSqlCreateForeignKeyConvertor extends Convertor { + can(statement: JsonStatement, dialect: Dialect): boolean { + return statement.type === 'create_reference' && dialect === 'googlesql'; + } + + convert(statement: JsonCreateReferenceStatement): string { + const { + name, + tableFrom, + tableTo, + 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};`; + } +} + class PgAlterForeignKeyConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'alter_reference' && dialect === 'postgresql'; @@ -3419,6 +4092,19 @@ class MySqlDeleteForeignKeyConvertor extends Convertor { } } +// TODO: SPANNER - verify +class GoogleSqlDeleteForeignKeyConvertor extends Convertor { + can(statement: JsonStatement, dialect: Dialect): boolean { + return statement.type === 'delete_reference' && dialect === 'googlesql'; + } + + 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`; + } +} + class CreatePgIndexConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'create_index_pg' && dialect === 'postgresql'; @@ -3500,6 +4186,33 @@ class CreateMySqlIndexConvertor extends Convertor { } } +// TODO: SPANNER - verify +class CreateGoogleSqlIndexConvertor extends Convertor { + can(statement: JsonStatement, dialect: Dialect): boolean { + return statement.type === 'create_index' && dialect === 'googlesql'; + } + + convert(statement: JsonCreateIndexStatement): string { + // should be changed + const { name, columns, isUnique } = GoogleSqlSquasher.unsquashIdx( + statement.data, + ); + const indexPart = isUnique ? 'UNIQUE INDEX' : 'INDEX'; + + const uniqueString = columns + .map((it) => { + return statement.internal?.indexes + ? statement.internal?.indexes[name]?.columns[it]?.isExpression + ? it + : `\`${it}\`` + : `\`${it}\``; + }) + .join(','); + + return `CREATE ${indexPart} \`${name}\` ON \`${statement.tableName}\` (${uniqueString});`; + } +} + export class CreateSingleStoreIndexConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'create_index' && dialect === 'singlestore'; @@ -3674,6 +4387,18 @@ class MySqlDropIndexConvertor extends Convertor { } } +// TODO: SPANNER - verify +class GoogleSqlDropIndexConvertor extends Convertor { + can(statement: JsonStatement, dialect: Dialect): boolean { + return statement.type === 'drop_index' && dialect === 'googlesql'; + } + + convert(statement: JsonDropIndexStatement): string { + const { name } = GoogleSqlSquasher.unsquashIdx(statement.data); + return `DROP INDEX \`${name}\` ON \`${statement.tableName}\`;`; + } +} + class SingleStoreDropIndexConvertor extends Convertor { can(statement: JsonStatement, dialect: Dialect): boolean { return statement.type === 'drop_index' && dialect === 'singlestore'; @@ -3879,6 +4604,7 @@ class SingleStoreRecreateTableConvertor extends Convertor { const convertors: Convertor[] = []; convertors.push(new PgCreateTableConvertor()); convertors.push(new MySqlCreateTableConvertor()); +convertors.push(new GoogleSqlCreateTableConvertor()); convertors.push(new SingleStoreCreateTableConvertor()); convertors.push(new SingleStoreRecreateTableConvertor()); convertors.push(new SQLiteCreateTableConvertor()); @@ -3895,9 +4621,13 @@ convertors.push(new PgAlterViewAlterTablespaceConvertor()); convertors.push(new PgAlterViewAlterUsingConvertor()); convertors.push(new MySqlCreateViewConvertor()); +convertors.push(new GoogleSqlCreateViewConvertor()); convertors.push(new MySqlDropViewConvertor()); +convertors.push(new GoogleSqlDropViewConvertor()); convertors.push(new MySqlRenameViewConvertor()); +convertors.push(new GoogleSqlRenameViewConvertor()); convertors.push(new MySqlAlterViewConvertor()); +convertors.push(new GoogleSqlAlterViewConvertor()); convertors.push(new SqliteCreateViewConvertor()); convertors.push(new SqliteDropViewConvertor()); @@ -3922,21 +4652,25 @@ convertors.push(new SQLiteDropTableConvertor()); convertors.push(new PgRenameTableConvertor()); convertors.push(new MySqlRenameTableConvertor()); +convertors.push(new GoogleSqlRenameTableConvertor()); convertors.push(new SingleStoreRenameTableConvertor()); convertors.push(new SqliteRenameTableConvertor()); convertors.push(new PgAlterTableRenameColumnConvertor()); convertors.push(new MySqlAlterTableRenameColumnConvertor()); +convertors.push(new GoogleSqlAlterTableRenameColumnConvertor()); convertors.push(new SingleStoreAlterTableRenameColumnConvertor()); convertors.push(new SQLiteAlterTableRenameColumnConvertor()); convertors.push(new PgAlterTableDropColumnConvertor()); convertors.push(new MySqlAlterTableDropColumnConvertor()); +convertors.push(new GoogleSqlAlterTableDropColumnConvertor()); convertors.push(new SingleStoreAlterTableDropColumnConvertor()); convertors.push(new SQLiteAlterTableDropColumnConvertor()); convertors.push(new PgAlterTableAddColumnConvertor()); convertors.push(new MySqlAlterTableAddColumnConvertor()); +convertors.push(new GoogleSqlAlterTableAddColumnConvertor()); convertors.push(new SingleStoreAlterTableAddColumnConvertor()); convertors.push(new SQLiteAlterTableAddColumnConvertor()); @@ -3948,22 +4682,28 @@ convertors.push(new PgAlterTableDropUniqueConstraintConvertor()); convertors.push(new PgAlterTableAddCheckConstraintConvertor()); convertors.push(new PgAlterTableDeleteCheckConstraintConvertor()); convertors.push(new MySqlAlterTableAddCheckConstraintConvertor()); +convertors.push(new GoogleSqlAlterTableAddCheckConstraintConvertor()); convertors.push(new MySqlAlterTableDeleteCheckConstraintConvertor()); +convertors.push(new GoogleSqlAlterTableDeleteCheckConstraintConvertor()); convertors.push(new MySQLAlterTableAddUniqueConstraintConvertor()); +convertors.push(new GoogleSqlAlterTableAddUniqueConstraintConvertor()); convertors.push(new MySQLAlterTableDropUniqueConstraintConvertor()); +convertors.push(new GoogleSqlAlterTableDropUniqueConstraintConvertor()); convertors.push(new SingleStoreAlterTableAddUniqueConstraintConvertor()); convertors.push(new SingleStoreAlterTableDropUniqueConstraintConvertor()); convertors.push(new CreatePgIndexConvertor()); convertors.push(new CreateMySqlIndexConvertor()); +convertors.push(new CreateGoogleSqlIndexConvertor()); convertors.push(new CreateSingleStoreIndexConvertor()); convertors.push(new CreateSqliteIndexConvertor()); convertors.push(new PgDropIndexConvertor()); convertors.push(new SqliteDropIndexConvertor()); convertors.push(new MySqlDropIndexConvertor()); +convertors.push(new GoogleSqlDropIndexConvertor()); convertors.push(new SingleStoreDropIndexConvertor()); convertors.push(new PgAlterTableAlterColumnSetPrimaryKeyConvertor()); @@ -3997,6 +4737,7 @@ convertors.push(new PgAlterTableAlterColumnDropGeneratedConvertor()); convertors.push(new PgAlterTableAlterColumnAlterrGeneratedConvertor()); convertors.push(new MySqlAlterTableAlterColumnAlterrGeneratedConvertor()); +convertors.push(new GoogleSqlAlterTableAlterColumnAlterGeneratedConvertor()); convertors.push(new SingleStoreAlterTableAlterColumnAlterrGeneratedConvertor()); @@ -4005,6 +4746,7 @@ convertors.push(new SqliteAlterTableAlterColumnAlterGeneratedConvertor()); convertors.push(new SqliteAlterTableAlterColumnSetExpressionConvertor()); convertors.push(new MySqlModifyColumn()); +convertors.push(new GoogleSqlModifyColumn()); convertors.push(new LibSQLModifyColumn()); // convertors.push(new MySqlAlterTableAlterColumnSetDefaultConvertor()); // convertors.push(new MySqlAlterTableAlterColumnDropDefaultConvertor()); @@ -4013,11 +4755,13 @@ convertors.push(new SingleStoreModifyColumn()); convertors.push(new PgCreateForeignKeyConvertor()); convertors.push(new MySqlCreateForeignKeyConvertor()); +convertors.push(new GoogleSqlCreateForeignKeyConvertor()); convertors.push(new PgAlterForeignKeyConvertor()); convertors.push(new PgDeleteForeignKeyConvertor()); convertors.push(new MySqlDeleteForeignKeyConvertor()); +convertors.push(new GoogleSqlDeleteForeignKeyConvertor()); convertors.push(new PgCreateSchemaConvertor()); convertors.push(new PgRenameSchemaConvertor()); @@ -4037,10 +4781,15 @@ convertors.push(new PgAlterTableDeleteCompositePrimaryKeyConvertor()); convertors.push(new PgAlterTableAlterCompositePrimaryKeyConvertor()); convertors.push(new MySqlAlterTableDeleteCompositePrimaryKeyConvertor()); +convertors.push(new GoogleSqlAlterTableCreateCompositePrimaryKeyConvertor()); convertors.push(new MySqlAlterTableDropPk()); +convertors.push(new GoogleSqlAlterTableDropPk()); convertors.push(new MySqlAlterTableCreateCompositePrimaryKeyConvertor()); +convertors.push(new GoogleSqlAlterTableDeleteCompositePrimaryKeyConvertor()); convertors.push(new MySqlAlterTableAddPk()); +convertors.push(new GoogleSqlAlterTableAddPk()); convertors.push(new MySqlAlterTableAlterCompositePrimaryKeyConvertor()); +convertors.push(new GoogleSqlAlterTableAlterCompositePrimaryKeyConvertor()); convertors.push(new SingleStoreAlterTableDropPk()); convertors.push(new SingleStoreAlterTableAddPk());