Files
drizzle-team--drizzle-orm/drizzle-kit/src/serializer/singlestoreSerializer.ts
T
2026-07-13 12:10:05 +08:00

768 lines
21 KiB
TypeScript

import chalk from 'chalk';
import { is, SQL } from 'drizzle-orm';
import {
AnySingleStoreTable,
getTableConfig,
type PrimaryKey as PrimaryKeyORM,
SingleStoreDialect,
uniqueKeyName,
} from 'drizzle-orm/singlestore-core';
import { RowDataPacket } from 'mysql2/promise';
import { withStyle } from '../cli/validations/outputs';
import { IntrospectStage, IntrospectStatus } from '../cli/views';
import { CasingType } from 'src/cli/validations/common';
import type { DB } from '../utils';
import {
Column,
Index,
PrimaryKey,
SingleStoreKitInternals,
SingleStoreSchemaInternal,
Table,
UniqueConstraint,
} from './singlestoreSchema';
import { sqlToStr } from './utils';
const dialect = new SingleStoreDialect();
export const indexName = (tableName: string, columns: string[]) => {
return `${tableName}_${columns.join('_')}_index`;
};
export const generateSingleStoreSnapshot = (
tables: AnySingleStoreTable[],
/* views: SingleStoreView[], */
casing: CasingType | undefined,
): SingleStoreSchemaInternal => {
const dialect = new SingleStoreDialect({ casing });
const result: Record<string, Table> = {};
/* const resultViews: Record<string, View> = {}; */
const internal: SingleStoreKitInternals = { tables: {}, indexes: {} };
for (const table of tables) {
const {
name: tableName,
columns,
indexes,
schema,
primaryKeys,
uniqueConstraints,
} = getTableConfig(table);
const columnsObject: Record<string, Column> = {};
const indexesObject: Record<string, Index> = {};
const primaryKeysObject: Record<string, PrimaryKey> = {};
const uniqueConstraintObject: Record<string, UniqueConstraint> = {};
columns.forEach((column) => {
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.primary) {
primaryKeysObject[`${tableName}_${column.name}`] = {
name: `${tableName}_${column.name}`,
columns: [column.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(
column.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 = `'${column.default}'`;
} else {
if (sqlTypeLowered === 'json' || Array.isArray(column.default)) {
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;
});
primaryKeys.map((pk: PrimaryKeyORM) => {
const columnNames = pk.columns.map((c: any) => c.name);
primaryKeysObject[pk.getName()] = {
name: pk.getName(),
columns: columnNames,
};
// all composite pk's should be treated as notNull
for (const column of pk.columns) {
columnsObject[column.name].notNull = true;
}
});
uniqueConstraints?.map((unq) => {
const columnNames = unq.columns.map((c) => c.name);
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,
};
});
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 `${it.name}`;
}
});
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);
}
}
indexesObject[name] = {
name,
columns: indexColumns,
isUnique: value.config.unique ?? false,
using: value.config.using,
algorithm: value.config.algorithm,
lock: value.config.lock,
};
});
// only handle tables without schemas
if (!schema) {
result[tableName] = {
name: tableName,
columns: columnsObject,
indexes: indexesObject,
compositePrimaryKeys: primaryKeysObject,
uniqueConstraints: uniqueConstraintObject,
};
}
}
/* for (const view of views) {
const {
isExisting,
name,
query,
schema,
selectedFields,
algorithm,
sqlSecurity,
withCheckOption,
} = getViewConfig(view);
const columnsObject: Record<string, Column> = {};
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], SingleStoreColumn)) {
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;
}
}
}
}
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: '1',
dialect: 'singlestore',
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 `('${resultDefault.substring(1, resultDefault.length - 1)}')`;
} else {
return `'${resultDefault}'`;
}
} 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<SingleStoreSchemaInternal> => {
const result: Record<string, Table> = {};
const internals: SingleStoreKitInternals = { 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 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 columnType = column["DATA_TYPE"];
const isPrimary = column['COLUMN_KEY'] === 'PRI'; // 'PRI', ''
let columnDefault: string | null = 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.startsWith('bigint(')
|| columnType.startsWith('tinyint(')
|| columnType.startsWith('date(')
|| columnType.startsWith('int(')
|| columnType.startsWith('mediumint(')
|| columnType.startsWith('smallint(')
|| columnType.startsWith('text(')
|| columnType.startsWith('time(')
|| columnType.startsWith('year(')
) {
changedType = columnType.replace(/\(\s*[^)]*\)$/, '');
}
if (columnType.includes('decimal(10,0)')) {
changedType = columnType.replace('decimal(10,0)', 'decimal');
}
if (columnDefault?.endsWith('.')) {
columnDefault = columnDefault.slice(0, -1);
}
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
? undefined
: /^-?[\d.]+(?:e-?\d+)?$/.test(columnDefault)
&& !['decimal', 'char', 'varchar'].some((type) => columnType.startsWith(type))
? Number(columnDefault)
: isDefaultAnExpression
? clearDefaults(columnDefault, collation)
: columnDefault.startsWith('CURRENT_TIMESTAMP')
? 'CURRENT_TIMESTAMP'
: `'${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: {},
uniqueConstraints: {},
};
} 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='UNIQUE'
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');
}
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],
};
}
}
}
/* const views = await db.query(
`select * from INFORMATION_SCHEMA.VIEWS WHERE table_schema = '${inputSchema}';`,
); */
/* const resultViews: Record<string, View> = {}; */
/* 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');
}
return {
version: '1',
dialect: 'singlestore',
tables: result,
/* views: resultViews, */
_meta: {
tables: {},
columns: {},
},
internal: internals,
};
};