diff --git a/drizzle-kit/src/cli/commands/migrate.ts b/drizzle-kit/src/cli/commands/migrate.ts index 96067c165..8c62a5edb 100644 --- a/drizzle-kit/src/cli/commands/migrate.ts +++ b/drizzle-kit/src/cli/commands/migrate.ts @@ -54,7 +54,7 @@ import { ResolveSelectNamed, schema, } from '../views'; -import { GenerateConfig } from './utils'; +import { ExportConfig, GenerateConfig } from './utils'; export type Named = { name: string; @@ -368,6 +368,44 @@ export const prepareAndMigratePg = async (config: GenerateConfig) => { } }; +export const prepareAndExportPg = async (config: ExportConfig) => { + const schemaPath = config.schema; + + try { + const { prev, cur } = await preparePgMigrationSnapshot( + [], // no snapshots before + schemaPath, + undefined, + ); + + const validatedPrev = pgSchema.parse(prev); + const validatedCur = pgSchema.parse(cur); + + const squashedPrev = squashPgScheme(validatedPrev); + const squashedCur = squashPgScheme(validatedCur); + + const { sqlStatements } = await applyPgSnapshotsDiff( + squashedPrev, + squashedCur, + schemasResolver, + enumsResolver, + sequencesResolver, + policyResolver, + indPolicyResolver, + roleResolver, + tablesResolver, + columnsResolver, + viewsResolver, + validatedPrev, + validatedCur, + ); + + console.log(sqlStatements.join('\n')); + } catch (e) { + console.error(e); + } +}; + export const preparePgPush = async ( cur: PgSchema, prev: PgSchema, @@ -697,6 +735,70 @@ export const prepareAndMigrateSingleStore = async (config: GenerateConfig) => { } }; +export const prepareAndExportSinglestore = async (config: ExportConfig) => { + const schemaPath = config.schema; + + try { + const { prev, cur } = await prepareSingleStoreMigrationSnapshot( + [], + schemaPath, + undefined, + ); + + const validatedPrev = singlestoreSchema.parse(prev); + const validatedCur = singlestoreSchema.parse(cur); + + const squashedPrev = squashSingleStoreScheme(validatedPrev); + const squashedCur = squashSingleStoreScheme(validatedCur); + + const { sqlStatements, _meta } = await applySingleStoreSnapshotsDiff( + squashedPrev, + squashedCur, + tablesResolver, + columnsResolver, + /* singleStoreViewsResolver, */ + validatedPrev, + validatedCur, + ); + + console.log(sqlStatements.join('\n')); + } catch (e) { + console.error(e); + } +}; + +export const prepareAndExportMysql = async (config: ExportConfig) => { + const schemaPath = config.schema; + + try { + const { prev, cur, custom } = await prepareMySqlMigrationSnapshot( + [], + schemaPath, + undefined, + ); + + const validatedPrev = mysqlSchema.parse(prev); + const validatedCur = mysqlSchema.parse(cur); + + const squashedPrev = squashMysqlScheme(validatedPrev); + const squashedCur = squashMysqlScheme(validatedCur); + + const { sqlStatements, statements, _meta } = await applyMysqlSnapshotsDiff( + squashedPrev, + squashedCur, + tablesResolver, + columnsResolver, + mySqlViewsResolver, + validatedPrev, + validatedCur, + ); + + console.log(sqlStatements.join('\n')); + } catch (e) { + console.error(e); + } +}; + export const prepareAndMigrateSqlite = async (config: GenerateConfig) => { const outFolder = config.out; const schemaPath = config.schema; @@ -760,6 +862,38 @@ export const prepareAndMigrateSqlite = async (config: GenerateConfig) => { } }; +export const prepareAndExportSqlite = async (config: ExportConfig) => { + const schemaPath = config.schema; + + try { + const { prev, cur } = await prepareSqliteMigrationSnapshot( + [], + schemaPath, + undefined, + ); + + const validatedPrev = sqliteSchema.parse(prev); + const validatedCur = sqliteSchema.parse(cur); + + const squashedPrev = squashSqliteScheme(validatedPrev); + const squashedCur = squashSqliteScheme(validatedCur); + + const { sqlStatements, _meta } = await applySqliteSnapshotsDiff( + squashedPrev, + squashedCur, + tablesResolver, + columnsResolver, + sqliteViewsResolver, + validatedPrev, + validatedCur, + ); + + console.log(sqlStatements.join('\n')); + } catch (e) { + console.error(e); + } +}; + export const prepareAndMigrateLibSQL = async (config: GenerateConfig) => { const outFolder = config.out; const schemaPath = config.schema; @@ -822,6 +956,38 @@ export const prepareAndMigrateLibSQL = async (config: GenerateConfig) => { } }; +export const prepareAndExportLibSQL = async (config: ExportConfig) => { + const schemaPath = config.schema; + + try { + const { prev, cur, custom } = await prepareSqliteMigrationSnapshot( + [], + schemaPath, + undefined, + ); + + const validatedPrev = sqliteSchema.parse(prev); + const validatedCur = sqliteSchema.parse(cur); + + const squashedPrev = squashSqliteScheme(validatedPrev); + const squashedCur = squashSqliteScheme(validatedCur); + + const { sqlStatements, _meta } = await applyLibSQLSnapshotsDiff( + squashedPrev, + squashedCur, + tablesResolver, + columnsResolver, + sqliteViewsResolver, + validatedPrev, + validatedCur, + ); + + console.log(sqlStatements.join('\n')); + } catch (e) { + console.error(e); + } +}; + export const prepareSQLitePush = async ( schemaPath: string | string[], snapshot: SQLiteSchema, diff --git a/drizzle-kit/src/cli/commands/utils.ts b/drizzle-kit/src/cli/commands/utils.ts index 60571ad73..cb5c75886 100644 --- a/drizzle-kit/src/cli/commands/utils.ts +++ b/drizzle-kit/src/cli/commands/utils.ts @@ -135,6 +135,12 @@ export type GenerateConfig = { driver?: Driver; }; +export type ExportConfig = { + dialect: Dialect; + schema: string | string[]; + sql: boolean; +}; + export const prepareGenerateConfig = async ( options: { config?: string; @@ -185,6 +191,38 @@ export const prepareGenerateConfig = async ( }; }; +export const prepareExportConfig = async ( + options: { + config?: string; + schema?: string; + dialect?: Dialect; + sql: boolean; + }, + from: 'config' | 'cli', +): Promise => { + const config = from === 'config' ? await drizzleConfigFromFile(options.config, true) : options; + + const { schema, dialect, sql } = config; + + if (!schema || !dialect) { + console.log(error('Please provide required params:')); + console.log(wrapParam('schema', schema)); + console.log(wrapParam('dialect', dialect)); + process.exit(1); + } + + const fileNames = prepareFilenames(schema); + if (fileNames.length === 0) { + render(`[${chalk.blue('i')}] No schema file in ${schema} was found`); + process.exit(0); + } + return { + dialect: dialect, + schema: schema, + sql: sql, + }; +}; + export const flattenDatabaseCredentials = (config: any) => { if ('dbCredentials' in config) { const { dbCredentials, ...rest } = config; @@ -768,6 +806,7 @@ export const prepareMigrateConfig = async (configPath: string | undefined) => { export const drizzleConfigFromFile = async ( configPath?: string, + isExport?: boolean, ): Promise => { const prefix = process.env.TEST_CONFIG_PATH_PREFIX || ''; @@ -783,7 +822,7 @@ export const drizzleConfigFromFile = async ( ? 'drizzle.config.js' : 'drizzle.config.json'; - if (!configPath) { + if (!configPath && !isExport) { console.log( chalk.gray( `No config path provided, using default '${defaultConfigPath}'`, @@ -798,7 +837,8 @@ export const drizzleConfigFromFile = async ( process.exit(1); } - console.log(chalk.grey(`Reading config file '${path}'`)); + if (!isExport) console.log(chalk.grey(`Reading config file '${path}'`)); + const { unregister } = await safeRegister(); const required = require(`${path}`); const content = required.default ?? required; diff --git a/drizzle-kit/src/cli/index.ts b/drizzle-kit/src/cli/index.ts index 86bffdf3d..42730be1d 100644 --- a/drizzle-kit/src/cli/index.ts +++ b/drizzle-kit/src/cli/index.ts @@ -1,6 +1,6 @@ import { command, run } from '@drizzle-team/brocli'; import chalk from 'chalk'; -import { check, drop, generate, migrate, pull, push, studio, up } from './schema'; +import { check, drop, exportRaw, generate, migrate, pull, push, studio, up } from './schema'; import { ormCoreVersions } from './utils'; const version = async () => { @@ -42,7 +42,7 @@ const legacy = [ legacyCommand('check:sqlite', 'check'), ]; -run([generate, migrate, pull, push, studio, up, check, drop, ...legacy], { +run([generate, migrate, pull, push, studio, up, check, drop, exportRaw, ...legacy], { name: 'drizzle-kit', version: version, }); diff --git a/drizzle-kit/src/cli/schema.ts b/drizzle-kit/src/cli/schema.ts index 40449dcdd..e4204e393 100644 --- a/drizzle-kit/src/cli/schema.ts +++ b/drizzle-kit/src/cli/schema.ts @@ -18,6 +18,7 @@ import { upSqliteHandler } from './commands/sqliteUp'; import { prepareCheckParams, prepareDropParams, + prepareExportConfig, prepareGenerateConfig, prepareMigrateConfig, preparePullConfig, @@ -747,3 +748,47 @@ export const studio = command({ } }, }); + +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 { + assertUnreachable(dialect); + } + }, +}); diff --git a/drizzle-kit/src/cli/validations/common.ts b/drizzle-kit/src/cli/validations/common.ts index 7fc6046a7..721f6effa 100644 --- a/drizzle-kit/src/cli/validations/common.ts +++ b/drizzle-kit/src/cli/validations/common.ts @@ -10,7 +10,8 @@ export type Commands = | 'check' | 'up' | 'drop' - | 'push'; + | 'push' + | 'export'; type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; type IsUnion = [T] extends [UnionToIntersection] ? false : true; @@ -111,6 +112,7 @@ export const configCommonSchema = object({ migrations: configMigrations, dbCredentials: any().optional(), casing: casingType.optional(), + sql: boolean().default(true), }).passthrough(); export const casing = union([literal('camel'), literal('preserve')]).default( diff --git a/drizzle-kit/tests/cli-export.test.ts b/drizzle-kit/tests/cli-export.test.ts new file mode 100644 index 000000000..8719ddd6a --- /dev/null +++ b/drizzle-kit/tests/cli-export.test.ts @@ -0,0 +1,79 @@ +import { test as brotest } from '@drizzle-team/brocli'; +import { assert, expect, test } from 'vitest'; +import { exportRaw } from '../src/cli/schema'; + +// good: +// #1 drizzle-kit export --dialect=postgresql --schema=schema.ts +// #3 drizzle-kit export +// #3 drizzle-kit export --config=drizzle1.config.ts + +// errors: +// #1 drizzle-kit export --schema=src/schema.ts +// #2 drizzle-kit export --dialect=postgresql +// #3 drizzle-kit export --dialect=postgresql2 +// #4 drizzle-kit export --config=drizzle.config.ts --schema=schema.ts +// #5 drizzle-kit export --config=drizzle.config.ts --dialect=postgresql + +test('export #1', async (t) => { + const res = await brotest( + exportRaw, + '--dialect=postgresql --schema=schema.ts', + ); + + if (res.type !== 'handler') assert.fail(res.type, 'handler'); + + expect(res.options).toStrictEqual({ + dialect: 'postgresql', + schema: 'schema.ts', + sql: true, + }); +}); + +test('export #2', async (t) => { + const res = await brotest(exportRaw, ''); + + if (res.type !== 'handler') assert.fail(res.type, 'handler'); + expect(res.options).toStrictEqual({ + dialect: 'postgresql', + schema: './schema.ts', + sql: true, + }); +}); + +// custom config path +test('export #3', async (t) => { + const res = await brotest(exportRaw, '--config=expo.config.ts'); + assert.equal(res.type, 'handler'); + if (res.type !== 'handler') assert.fail(res.type, 'handler'); + expect(res.options).toStrictEqual({ + dialect: 'sqlite', + schema: './schema.ts', + sql: true, + }); +}); + +// --- errors --- +test('err #1', async (t) => { + const res = await brotest(exportRaw, '--schema=src/schema.ts'); + assert.equal(res.type, 'error'); +}); + +test('err #2', async (t) => { + const res = await brotest(exportRaw, '--dialect=postgresql'); + assert.equal(res.type, 'error'); +}); + +test('err #3', async (t) => { + const res = await brotest(exportRaw, '--dialect=postgresql2'); + assert.equal(res.type, 'error'); +}); + +test('err #4', async (t) => { + const res = await brotest(exportRaw, '--config=drizzle.config.ts --schema=schema.ts'); + assert.equal(res.type, 'error'); +}); + +test('err #5', async (t) => { + const res = await brotest(exportRaw, '--config=drizzle.config.ts --dialect=postgresql'); + assert.equal(res.type, 'error'); +});