From 4cd9dfb83cc38e49a4788450919cec38360878c0 Mon Sep 17 00:00:00 2001 From: Mario564 Date: Thu, 3 Oct 2024 09:12:21 -0700 Subject: [PATCH] Add SQLite schema validation checks --- .../src/serializer/sqliteSerializer.ts | 81 +------- drizzle-kit/src/validate-schema/validate.ts | 179 +++++++++++++++++- 2 files changed, 179 insertions(+), 81 deletions(-) diff --git a/drizzle-kit/src/serializer/sqliteSerializer.ts b/drizzle-kit/src/serializer/sqliteSerializer.ts index f1d28f759..cff8c3c95 100644 --- a/drizzle-kit/src/serializer/sqliteSerializer.ts +++ b/drizzle-kit/src/serializer/sqliteSerializer.ts @@ -1,4 +1,3 @@ -import chalk from 'chalk'; import { getTableName, is, SQL } from 'drizzle-orm'; import { toCamelCase, toSnakeCase } from 'drizzle-orm/casing'; import { @@ -22,7 +21,7 @@ import type { Table, UniqueConstraint, } from '../serializer/sqliteSchema'; -import { getColumnCasing, type SQLiteDB } from '../utils'; +import { getColumnCasing, getForeignKeyName, getPrimaryKeyName, type SQLiteDB } from '../utils'; import { sqlToStr } from '.'; export const generateSqliteSnapshot = ( @@ -90,32 +89,6 @@ export const generateSqliteSnapshot = ( columnsObject[name] = columnToSet; if (column.isUnique) { - const existingUnique = indexesObject[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); - } indexesObject[column.uniqueName!] = { name: column.uniqueName!, columns: [columnToSet.name], @@ -125,6 +98,7 @@ export const generateSqliteSnapshot = ( }); const foreignKeys: ForeignKey[] = tableForeignKeys.map((fk) => { + const name = getForeignKeyName(fk, casing); const tableFrom = tableName; const onDelete = fk.onDelete ?? 'no action'; const onUpdate = fk.onUpdate ?? 'no action'; @@ -134,22 +108,9 @@ export const generateSqliteSnapshot = ( // 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, @@ -212,37 +173,8 @@ export const generateSqliteSnapshot = ( uniqueConstraints?.map((unq) => { const columnNames = unq.columns.map((c) => getColumnCasing(c, casing)); - const name = unq.name ?? uniqueKeyName(table, columnNames); - const existingUnique = indexesObject[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); - } - indexesObject[name] = { name: unq.name!, columns: columnNames, @@ -252,16 +184,9 @@ export const generateSqliteSnapshot = ( primaryKeys.forEach((it) => { if (it.columns.length > 1) { - const originalColumnNames = it.columns.map((c) => c.name); + const name = getPrimaryKeyName(it, casing); const columnNames = it.columns.map((c) => getColumnCasing(c, casing)); - let name = it.getName(); - if (casing !== undefined) { - for (let i = 0; i < originalColumnNames.length; i++) { - name = name.replace(originalColumnNames[i], columnNames[i]); - } - } - primaryKeysObject[name] = { columns: columnNames, name, diff --git a/drizzle-kit/src/validate-schema/validate.ts b/drizzle-kit/src/validate-schema/validate.ts index 947920c32..e248185c1 100644 --- a/drizzle-kit/src/validate-schema/validate.ts +++ b/drizzle-kit/src/validate-schema/validate.ts @@ -1,14 +1,14 @@ +import chalk from 'chalk'; import { getTableConfig as getPgTableConfig, getViewConfig as getPgViewConfig, getMaterializedViewConfig as getPgMaterializedViewConfig, PgEnum, PgMaterializedView, PgSchema, PgSequence, PgTable, PgView, IndexedColumn as PgIndexColumn, uniqueKeyName as pgUniqueKeyName, PgColumn, PgDialect } from 'drizzle-orm/pg-core'; import { Sequence as SequenceCommon, Table as TableCommon } from './utils'; -import { MySqlDialect, MySqlTable, MySqlView, getTableConfig as getMySqlTableConfig, getViewConfig as getMySqlViewConfig, IndexColumn as MySqlIndexColumn, MySqlColumn, uniqueKeyName as mysqlUniqueKeyName } from 'drizzle-orm/mysql-core'; -import { SQLiteTable, SQLiteView } from 'drizzle-orm/sqlite-core'; +import { MySqlTable, MySqlView, getTableConfig as getMySqlTableConfig, getViewConfig as getMySqlViewConfig, MySqlColumn, uniqueKeyName as mysqlUniqueKeyName } from 'drizzle-orm/mysql-core'; +import { SQLiteColumn, SQLiteTable, SQLiteView, getTableConfig as getSQLiteTableConfig, getViewConfig as getSQLiteViewConfig, uniqueKeyName as sqliteUniqueKeyName } from 'drizzle-orm/sqlite-core'; import { GeneratedIdentityConfig, getTableName, is, SQL } from 'drizzle-orm'; import { ValidateDatabase } from './db'; import { CasingType } from 'src/cli/validations/common'; import { getColumnCasing, getForeignKeyName, getIdentitySequenceName, getPrimaryKeyName } from 'src/utils'; import { indexName as pgIndexName } from 'src/serializer/pgSerializer'; import { indexName as mysqlIndexName } from 'src/serializer/mysqlSerializer'; -import chalk from 'chalk'; import { render } from 'hanji'; import { ValidationError } from './errors'; @@ -454,5 +454,178 @@ export function validateSQLiteSchema( tables: SQLiteTable[], views: SQLiteView[], ) { + const tableConfigs = tables.map((table) => getSQLiteTableConfig(table)); + const viewConfigs = views.map((view) => getSQLiteViewConfig(view)); + const group = (() => { + const dbTables = tableConfigs + .map((table) => ({ + ...table, + columns: table.columns.map((column) => ({ + ...column, + name: getColumnCasing(column, casing) + })) + })); + + const dbViews = viewConfigs; + + const dbIndexes = dbTables.map( + (table) => table.indexes.map( + (index) => { + const indexColumns = index.config.columns + .filter((column): column is SQLiteColumn => !is(column, SQL)); + + const indexColumnNames = indexColumns + .map((column) => column.name) + .filter((c) => c !== undefined); + + return { + name: index.config.name + ? index.config.name + : indexColumns.length === index.config.columns.length + ? mysqlIndexName(table.name, indexColumnNames) + : '', + columns: index.config.columns.map((column) => { + if (is(column, SQL)) { + return column; + } + + return { + type: '', + name: getColumnCasing(column as SQLiteColumn, casing), + } + }) + }; + } + ) + ).flat(1) satisfies TableCommon['indexes']; + + const dbForeignKeys = dbTables.map( + (table) => table.foreignKeys.map( + (fk) => { + const ref = fk.reference(); + return { + name: getForeignKeyName(fk, casing), + reference: { + columns: ref.columns.map( + (column) => { + const tableConfig = getSQLiteTableConfig(column.table); + return { + name: getColumnCasing(column, casing), + sqlType: column.getSQLType(), + table: { + name: tableConfig.name + } + }; + } + ), + foreignColumns: ref.foreignColumns.map( + (column) => { + const tableConfig = getSQLiteTableConfig(column.table); + return { + name: getColumnCasing(column, casing), + sqlType: column.getSQLType(), + table: { + name: tableConfig.name + } + }; + } + ), + } + }; + } + ) + ).flat(1) satisfies TableCommon['foreignKeys']; + + const dbChecks = dbTables.map( + (table) => table.checks.map( + (check) => ({ + name: check.name + }) + ) + ).flat(1) satisfies TableCommon['checks']; + + const dbPrimaryKeys = dbTables.map( + (table) => table.primaryKeys.map( + (pk) => ({ + name: getPrimaryKeyName(pk, casing), + columns: pk.columns.map( + (column) => { + const tableConfig = getSQLiteTableConfig(column.table); + return { + name: getColumnCasing(column, casing), + table: { + name: tableConfig.name + } + } + } + ) + }) + ) + ).flat(1) satisfies TableCommon['primaryKeys']; + + const dbUniqueConstraints = dbTables.map( + (table) => table.uniqueConstraints.map( + (unique) => { + const columnNames = unique.columns.map((column) => getColumnCasing(column, casing)); + + return { + name: unique.name ?? sqliteUniqueKeyName(tables.find((t) => getTableName(t) === table.name)!, columnNames) + }; + } + ) + ).flat(1) satisfies TableCommon['uniqueConstraints']; + + return { + tables: dbTables, + views: dbViews, + indexes: dbIndexes, + foreignKeys: dbForeignKeys, + checks: dbChecks, + primaryKeys: dbPrimaryKeys, + uniqueConstraints: dbUniqueConstraints + }; + })(); + + const vDb = new ValidateDatabase(); + const v = vDb.validateSchema(undefined); + + v + .constraintNameCollisions( + group.indexes, + group.foreignKeys, + group.checks, + group.primaryKeys, + group.uniqueConstraints + ) + .entityNameCollisions( + group.tables, + group.views, + [], + [], + [] + ); + + for (const table of group.tables) { + v.validateTable(table.name).columnNameCollisions(table.columns, casing); + } + + for (const foreignKey of group.foreignKeys) { + v + .validateForeignKey(foreignKey.name) + .columnsMixingTables(foreignKey) + .mismatchingColumnCount(foreignKey) + .mismatchingDataTypes(foreignKey, 'sqlite'); + } + + for (const primaryKey of group.primaryKeys) { + v + .validatePrimaryKey(primaryKey.name) + .columnsMixingTables(primaryKey); + } + + return { + messages: vDb.errors, + codes: vDb.errorCodes + }; } \ No newline at end of file