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

857 lines
21 KiB
TypeScript

import { boolean, command, number, string } from '@drizzle-team/brocli';
import chalk from 'chalk';
import 'dotenv/config';
import { mkdirSync } from 'fs';
import { renderWithTask } from 'hanji';
import { dialects } from 'src/schemaValidator';
import '../@types/utils';
import { assertUnreachable } from '../global';
import type { Setup } from '../serializer/studio';
import { assertV1OutFolder } from '../utils';
import { certs } from '../utils/certs';
import { checkHandler } from './commands/check';
import { dropMigration } from './commands/drop';
import { upMysqlHandler } from './commands/mysqlUp';
import { upPgHandler } from './commands/pgUp';
import { upSinglestoreHandler } from './commands/singlestoreUp';
import { upSqliteHandler } from './commands/sqliteUp';
import {
prepareCheckParams,
prepareDropParams,
prepareExportConfig,
prepareGenerateConfig,
prepareMigrateConfig,
preparePullConfig,
preparePushConfig,
prepareStudioConfig,
} from './commands/utils';
import { assertOrmCoreVersion, assertPackages, assertStudioNodeVersion, ormVersionGt } from './utils';
import { assertCollisions, drivers, prefixes } from './validations/common';
import { withStyle } from './validations/outputs';
import { error, grey, MigrateProgress } from './views';
const optionDialect = string('dialect')
.enum(...dialects)
.desc(
`Database dialect: 'gel', 'postgresql', 'mysql', 'sqlite', 'turso' or 'singlestore'`,
);
const optionOut = string().desc("Output folder, 'drizzle' by default");
const optionConfig = string().desc('Path to drizzle config file');
const optionBreakpoints = boolean().desc(
`Prepare SQL statements with breakpoints`,
);
const optionDriver = string()
.enum(...drivers)
.desc('Database driver');
const optionCasing = string().enum('camelCase', 'snake_case').desc('Casing for serialization');
export const generate = command({
name: 'generate',
options: {
config: optionConfig,
dialect: optionDialect,
driver: optionDriver,
casing: optionCasing,
schema: string().desc('Path to a schema file or folder'),
out: optionOut,
name: string().desc('Migration file name'),
breakpoints: optionBreakpoints,
custom: boolean()
.desc('Prepare empty migration file for custom SQL')
.default(false),
prefix: string()
.enum(...prefixes)
.default('index'),
},
transform: async (opts) => {
const from = assertCollisions(
'generate',
opts,
['prefix', 'name', 'custom'],
['driver', 'breakpoints', 'schema', 'out', 'dialect', 'casing'],
);
return prepareGenerateConfig(opts, from);
},
handler: async (opts) => {
await assertOrmCoreVersion();
await assertPackages('drizzle-orm');
// const parsed = cliConfigGenerate.parse(opts);
const {
prepareAndMigratePg,
prepareAndMigrateMysql,
prepareAndMigrateSqlite,
prepareAndMigrateLibSQL,
prepareAndMigrateSingleStore,
} = await import('./commands/migrate');
const dialect = opts.dialect;
if (dialect === 'postgresql') {
await prepareAndMigratePg(opts);
} else if (dialect === 'mysql') {
await prepareAndMigrateMysql(opts);
} else if (dialect === 'sqlite') {
await prepareAndMigrateSqlite(opts);
} else if (dialect === 'turso') {
await prepareAndMigrateLibSQL(opts);
} else if (dialect === 'singlestore') {
await prepareAndMigrateSingleStore(opts);
} else if (dialect === 'gel') {
console.log(
error(
`You can't use 'generate' command with Gel dialect`,
),
);
process.exit(1);
} else {
assertUnreachable(dialect);
}
},
});
export const migrate = command({
name: 'migrate',
options: {
config: optionConfig,
},
transform: async (opts) => {
return await prepareMigrateConfig(opts.config);
},
handler: async (opts) => {
await assertOrmCoreVersion();
await assertPackages('drizzle-orm');
const { dialect, schema, table, out, credentials } = opts;
try {
if (dialect === 'postgresql') {
if ('driver' in credentials) {
const { driver } = credentials;
if (driver === 'aws-data-api') {
if (!(await ormVersionGt('0.30.10'))) {
console.log(
"To use 'aws-data-api' driver - please update drizzle-orm to the latest version",
);
process.exit(1);
}
} else if (driver === 'pglite') {
if (!(await ormVersionGt('0.30.6'))) {
console.log(
"To use 'pglite' driver - please update drizzle-orm to the latest version",
);
process.exit(1);
}
} else {
assertUnreachable(driver);
}
}
const { preparePostgresDB } = await import('./connections');
const { migrate } = await preparePostgresDB(credentials);
await renderWithTask(
new MigrateProgress(),
migrate({
migrationsFolder: out,
migrationsTable: table,
migrationsSchema: schema,
}),
);
} else if (dialect === 'mysql') {
const { connectToMySQL } = await import('./connections');
const { migrate } = await connectToMySQL(credentials);
await renderWithTask(
new MigrateProgress(),
migrate({
migrationsFolder: out,
migrationsTable: table,
migrationsSchema: schema,
}),
);
} else if (dialect === 'singlestore') {
const { connectToSingleStore } = await import('./connections');
const { migrate } = await connectToSingleStore(credentials);
await renderWithTask(
new MigrateProgress(),
migrate({
migrationsFolder: out,
migrationsTable: table,
migrationsSchema: schema,
}),
);
} else if (dialect === 'sqlite') {
const { connectToSQLite } = await import('./connections');
const { migrate } = await connectToSQLite(credentials);
await renderWithTask(
new MigrateProgress(),
migrate({
migrationsFolder: opts.out,
migrationsTable: table,
migrationsSchema: schema,
}),
);
} else if (dialect === 'turso') {
const { connectToLibSQL } = await import('./connections');
const { migrate } = await connectToLibSQL(credentials);
await renderWithTask(
new MigrateProgress(),
migrate({
migrationsFolder: opts.out,
migrationsTable: table,
migrationsSchema: schema,
}),
);
} else if (dialect === 'gel') {
console.log(
error(
`You can't use 'migrate' command with Gel dialect`,
),
);
process.exit(1);
} else {
assertUnreachable(dialect);
}
} catch (e) {
console.error(e);
process.exit(1);
}
process.exit(0);
},
});
const optionsFilters = {
tablesFilter: string().desc('Table name filters'),
schemaFilters: string().desc('Schema name filters'),
extensionsFilters: string().desc(
'`Database extensions internal database filters',
),
} as const;
const optionsDatabaseCredentials = {
url: string().desc('Database connection URL'),
host: string().desc('Database host'),
port: string().desc('Database port'),
user: string().desc('Database user'),
password: string().desc('Database password'),
database: string().desc('Database name'),
ssl: string().desc('ssl mode'),
// Turso
authToken: string('auth-token').desc('Database auth token [Turso]'),
// gel
tlsSecurity: string('tlsSecurity').desc('tls security mode'),
// specific cases
driver: optionDriver,
} as const;
export const push = command({
name: 'push',
options: {
config: optionConfig,
dialect: optionDialect,
casing: optionCasing,
schema: string().desc('Path to a schema file or folder'),
...optionsFilters,
...optionsDatabaseCredentials,
verbose: boolean()
.desc('Print all statements for each push')
.default(false),
strict: boolean().desc('Always ask for confirmation').default(false),
force: boolean()
.desc(
'Auto-approve all data loss statements. Note: Data loss statements may truncate your tables and data',
)
.default(false),
},
transform: async (opts) => {
const from = assertCollisions(
'push',
opts,
['force', 'verbose', 'strict'],
[
'schema',
'dialect',
'driver',
'url',
'host',
'port',
'user',
'password',
'database',
'ssl',
'authToken',
'schemaFilters',
'extensionsFilters',
'tablesFilter',
'casing',
'tlsSecurity',
],
);
return preparePushConfig(opts, from);
},
handler: async (config) => {
await assertPackages('drizzle-orm');
await assertOrmCoreVersion();
const {
dialect,
schemaPath,
strict,
verbose,
credentials,
tablesFilter,
schemasFilter,
force,
casing,
entities,
} = config;
try {
if (dialect === 'mysql') {
const { mysqlPush } = await import('./commands/push');
await mysqlPush(
schemaPath,
credentials,
tablesFilter,
strict,
verbose,
force,
casing,
);
} else if (dialect === 'postgresql') {
if ('driver' in credentials) {
const { driver } = credentials;
if (driver === 'aws-data-api') {
if (!(await ormVersionGt('0.30.10'))) {
console.log(
"To use 'aws-data-api' driver - please update drizzle-orm to the latest version",
);
process.exit(1);
}
} else if (driver === 'pglite') {
if (!(await ormVersionGt('0.30.6'))) {
console.log(
"To use 'pglite' driver - please update drizzle-orm to the latest version",
);
process.exit(1);
}
} else {
assertUnreachable(driver);
}
}
const { pgPush } = await import('./commands/push');
await pgPush(
schemaPath,
verbose,
strict,
credentials,
tablesFilter,
schemasFilter,
entities,
force,
casing,
);
} else if (dialect === 'sqlite') {
const { sqlitePush } = await import('./commands/push');
await sqlitePush(
schemaPath,
verbose,
strict,
credentials,
tablesFilter,
force,
casing,
);
} else if (dialect === 'turso') {
const { libSQLPush } = await import('./commands/push');
await libSQLPush(
schemaPath,
verbose,
strict,
credentials,
tablesFilter,
force,
casing,
);
} else if (dialect === 'singlestore') {
const { singlestorePush } = await import('./commands/push');
await singlestorePush(
schemaPath,
credentials,
tablesFilter,
strict,
verbose,
force,
casing,
);
} else if (dialect === 'gel') {
console.log(
error(
`You can't use 'push' command with Gel dialect`,
),
);
process.exit(1);
} else {
assertUnreachable(dialect);
}
} catch (e) {
console.error(e);
}
process.exit(0);
},
});
export const check = command({
name: 'check',
options: {
config: optionConfig,
dialect: optionDialect,
out: optionOut,
},
transform: async (opts) => {
const from = assertCollisions('check', opts, [], ['dialect', 'out']);
return prepareCheckParams(opts, from);
},
handler: async (config) => {
await assertOrmCoreVersion();
const { out, dialect } = config;
checkHandler(out, dialect);
console.log("Everything's fine 🐶🔥");
},
});
export const up = command({
name: 'up',
options: {
config: optionConfig,
dialect: optionDialect,
out: optionOut,
},
transform: async (opts) => {
const from = assertCollisions('check', opts, [], ['dialect', 'out']);
return prepareCheckParams(opts, from);
},
handler: async (config) => {
await assertOrmCoreVersion();
const { out, dialect } = config;
await assertPackages('drizzle-orm');
if (dialect === 'postgresql') {
upPgHandler(out);
}
if (dialect === 'mysql') {
upMysqlHandler(out);
}
if (dialect === 'sqlite' || dialect === 'turso') {
upSqliteHandler(out);
}
if (dialect === 'singlestore') {
upSinglestoreHandler(out);
}
if (dialect === 'gel') {
console.log(
error(
`You can't use 'up' command with Gel dialect`,
),
);
process.exit(1);
}
},
});
export const pull = command({
name: 'introspect',
aliases: ['pull'],
options: {
config: optionConfig,
dialect: optionDialect,
out: optionOut,
breakpoints: optionBreakpoints,
casing: string('introspect-casing').enum('camel', 'preserve'),
...optionsFilters,
...optionsDatabaseCredentials,
},
transform: async (opts) => {
const from = assertCollisions(
'introspect',
opts,
[],
[
'dialect',
'driver',
'out',
'url',
'host',
'port',
'user',
'password',
'database',
'ssl',
'authToken',
'casing',
'breakpoints',
'tablesFilter',
'schemaFilters',
'extensionsFilters',
'tlsSecurity',
],
);
return preparePullConfig(opts, from);
},
handler: async (config) => {
await assertPackages('drizzle-orm');
await assertOrmCoreVersion();
const {
dialect,
credentials,
out,
casing,
breakpoints,
tablesFilter,
schemasFilter,
prefix,
entities,
} = config;
mkdirSync(out, { recursive: true });
console.log(
grey(
`Pulling from [${
schemasFilter
.map((it) => `'${it}'`)
.join(', ')
}] list of schemas`,
),
);
console.log();
try {
if (dialect === 'postgresql') {
if ('driver' in credentials) {
const { driver } = credentials;
if (driver === 'aws-data-api') {
if (!(await ormVersionGt('0.30.10'))) {
console.log(
"To use 'aws-data-api' driver - please update drizzle-orm to the latest version",
);
process.exit(1);
}
} else if (driver === 'pglite') {
if (!(await ormVersionGt('0.30.6'))) {
console.log(
"To use 'pglite' driver - please update drizzle-orm to the latest version",
);
process.exit(1);
}
} else {
assertUnreachable(driver);
}
}
const { introspectPostgres } = await import('./commands/introspect');
await introspectPostgres(
casing,
out,
breakpoints,
credentials,
tablesFilter,
schemasFilter,
prefix,
entities,
);
} else if (dialect === 'mysql') {
const { introspectMysql } = await import('./commands/introspect');
await introspectMysql(
casing,
out,
breakpoints,
credentials,
tablesFilter,
prefix,
);
} else if (dialect === 'sqlite') {
const { introspectSqlite } = await import('./commands/introspect');
await introspectSqlite(
casing,
out,
breakpoints,
credentials,
tablesFilter,
prefix,
);
} else if (dialect === 'turso') {
const { introspectLibSQL } = await import('./commands/introspect');
await introspectLibSQL(
casing,
out,
breakpoints,
credentials,
tablesFilter,
prefix,
);
} else if (dialect === 'singlestore') {
const { introspectSingleStore } = await import('./commands/introspect');
await introspectSingleStore(
casing,
out,
breakpoints,
credentials,
tablesFilter,
prefix,
);
} else if (dialect === 'gel') {
const { introspectGel } = await import('./commands/introspect');
await introspectGel(
casing,
out,
breakpoints,
credentials,
tablesFilter,
schemasFilter,
prefix,
entities,
);
} else {
assertUnreachable(dialect);
}
} catch (e) {
console.error(e);
}
process.exit(0);
},
});
export const drop = command({
name: 'drop',
options: {
config: optionConfig,
out: optionOut,
driver: optionDriver,
},
transform: async (opts) => {
const from = assertCollisions('check', opts, [], ['driver', 'out']);
return prepareDropParams(opts, from);
},
handler: async (config) => {
await assertOrmCoreVersion();
assertV1OutFolder(config.out);
await dropMigration(config);
},
});
export const studio = command({
name: 'studio',
options: {
config: optionConfig,
port: number().desc('Custom port for drizzle studio [default=4983]'),
host: string().desc('Custom host for drizzle studio [default=0.0.0.0]'),
verbose: boolean()
.default(false)
.desc('Print all stataments that are executed by Studio'),
},
handler: async (opts) => {
await assertOrmCoreVersion();
await assertPackages('drizzle-orm');
assertStudioNodeVersion();
const {
dialect,
schema: schemaPath,
port,
host,
credentials,
casing,
} = await prepareStudioConfig(opts);
const {
drizzleForPostgres,
preparePgSchema,
prepareMySqlSchema,
drizzleForMySQL,
prepareSQLiteSchema,
drizzleForSQLite,
prepareSingleStoreSchema,
drizzleForSingleStore,
drizzleForLibSQL,
} = await import('../serializer/studio');
let setup: Setup;
try {
if (dialect === 'postgresql') {
if ('driver' in credentials) {
const { driver } = credentials;
if (driver === 'aws-data-api') {
if (!(await ormVersionGt('0.30.10'))) {
console.log(
"To use 'aws-data-api' driver - please update drizzle-orm to the latest version",
);
process.exit(1);
}
} else if (driver === 'pglite') {
if (!(await ormVersionGt('0.30.6'))) {
console.log(
"To use 'pglite' driver - please update drizzle-orm to the latest version",
);
process.exit(1);
}
} else {
assertUnreachable(driver);
}
}
const { schema, relations, files } = schemaPath
? await preparePgSchema(schemaPath)
: { schema: {}, relations: {}, files: [] };
setup = await drizzleForPostgres(credentials, schema, relations, files, casing);
} else if (dialect === 'mysql') {
const { schema, relations, files } = schemaPath
? await prepareMySqlSchema(schemaPath)
: { schema: {}, relations: {}, files: [] };
setup = await drizzleForMySQL(credentials, schema, relations, files, casing);
} else if (dialect === 'sqlite') {
const { schema, relations, files } = schemaPath
? await prepareSQLiteSchema(schemaPath)
: { schema: {}, relations: {}, files: [] };
setup = await drizzleForSQLite(credentials, schema, relations, files, casing);
} else if (dialect === 'turso') {
const { schema, relations, files } = schemaPath
? await prepareSQLiteSchema(schemaPath)
: { schema: {}, relations: {}, files: [] };
setup = await drizzleForLibSQL(credentials, schema, relations, files, casing);
} else if (dialect === 'singlestore') {
const { schema, relations, files } = schemaPath
? await prepareSingleStoreSchema(schemaPath)
: { schema: {}, relations: {}, files: [] };
setup = await drizzleForSingleStore(
credentials,
schema,
relations,
files,
casing,
);
} else if (dialect === 'gel') {
console.log(
error(
`You can't use 'studio' command with Gel dialect`,
),
);
process.exit(1);
} else {
assertUnreachable(dialect);
}
const { prepareServer } = await import('../serializer/studio');
const server = await prepareServer(setup);
console.log();
console.log(
withStyle.fullWarning(
'Drizzle Studio is currently in Beta. If you find anything that is not working as expected or should be improved, feel free to create an issue on GitHub: https://github.com/drizzle-team/drizzle-kit-mirror/issues/new or write to us on Discord: https://discord.gg/WcRKz2FFxN',
),
);
const { key, cert } = (await certs()) || {};
server.start({
host,
port,
key,
cert,
cb: (err, _address) => {
if (err) {
console.error(err);
} else {
const queryParams: { port?: number; host?: string } = {};
if (port !== 4983) {
queryParams.port = port;
}
if (host !== '127.0.0.1') {
queryParams.host = host;
}
const queryString = Object.keys(queryParams)
.map((key: keyof { port?: number; host?: string }) => {
return `${key}=${queryParams[key]}`;
})
.join('&');
console.log(
`\nDrizzle Studio is up and running on ${
chalk.blue(
`https://local.drizzle.studio${queryString ? `?${queryString}` : ''}`,
)
}`,
);
}
},
});
} catch (e) {
console.error(e);
process.exit(0);
}
},
});
export const exportRaw = command({
name: 'export',
desc: 'Generate diff between current state and empty state in specified formats: sql',
options: {
sql: boolean('sql').default(true).desc('Generate as sql'),
config: optionConfig,
dialect: optionDialect,
schema: string().desc('Path to a schema file or folder'),
},
transform: async (opts) => {
const from = assertCollisions('export', opts, ['sql'], ['dialect', 'schema']);
return prepareExportConfig(opts, from);
},
handler: async (opts) => {
await assertOrmCoreVersion();
await assertPackages('drizzle-orm');
const {
prepareAndExportPg,
prepareAndExportMysql,
prepareAndExportSqlite,
prepareAndExportLibSQL,
prepareAndExportSinglestore,
} = await import(
'./commands/migrate'
);
const dialect = opts.dialect;
if (dialect === 'postgresql') {
await prepareAndExportPg(opts);
} else if (dialect === 'mysql') {
await prepareAndExportMysql(opts);
} else if (dialect === 'sqlite') {
await prepareAndExportSqlite(opts);
} else if (dialect === 'turso') {
await prepareAndExportLibSQL(opts);
} else if (dialect === 'singlestore') {
await prepareAndExportSinglestore(opts);
} else if (dialect === 'gel') {
console.log(
error(
`You can't use 'export' command with Gel dialect`,
),
);
process.exit(1);
} else {
assertUnreachable(dialect);
}
},
});