Skip to content

Commit

Permalink
Add SQLite schema validation checks
Browse files Browse the repository at this point in the history
  • Loading branch information
L-Mario564 committed Oct 3, 2024
1 parent def561b commit 4cd9dfb
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 81 deletions.
81 changes: 3 additions & 78 deletions drizzle-kit/src/serializer/sqliteSerializer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import chalk from 'chalk';
import { getTableName, is, SQL } from 'drizzle-orm';
import { toCamelCase, toSnakeCase } from 'drizzle-orm/casing';
import {
Expand All @@ -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 = (
Expand Down Expand Up @@ -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],
Expand All @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
179 changes: 176 additions & 3 deletions drizzle-kit/src/validate-schema/validate.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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
};
}

0 comments on commit 4cd9dfb

Please sign in to comment.