diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000..2fc10618e --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(rm:*)", + "Bash(mv:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 73acd5048..428f3bae8 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -1,22 +1,16 @@ import { CLIOptions, Inquirerer, Question } from 'inquirerer'; import { ParsedArgs } from 'minimist'; -import { - errors, - getEnvOptions, - LaunchQLOptions -} from '@launchql/types'; import { getPgEnvOptions, getSpawnEnvWithPg, } from 'pg-env'; -import { deploy, deployFast } from '@launchql/core'; +import { deployModules } from '@launchql/core'; import { Logger } from '@launchql/logger'; import { execSync } from 'child_process'; -import { LaunchQLProject } from '@launchql/core'; -import { deployCommand } from '@launchql/migrate'; import { getTargetDatabase } from '../utils'; +import { selectModule } from '../utils/module-utils'; export default async ( argv: Partial, @@ -71,8 +65,6 @@ export default async ( log.debug(`Using current directory: ${cwd}`); - const project = new LaunchQLProject(cwd); - if (createdb) { log.info(`Creating database ${database}...`); execSync(`createdb ${database}`, { @@ -80,68 +72,25 @@ export default async ( }); } - const options: LaunchQLOptions = getEnvOptions({ - pg: { - database - } - }); - + let projectName: string | undefined; if (recursive) { - const modules = await project.getModules(); - const moduleNames = modules.map(mod => mod.getModuleName()); - - if (!moduleNames.length) { - log.error('No modules found in the specified directory.'); - prompter.close(); - throw errors.NOT_FOUND({}, 'No modules found in the specified directory.'); - } - - const { project: selectedProject } = await prompter.prompt(argv, [ - { - type: 'autocomplete', - name: 'project', - message: 'Choose a project to deploy', - options: moduleNames, - required: true - } - ]); - - const selected = modules.find(mod => mod.getModuleName() === selectedProject); - if (!selected) { - throw new Error(`Module ${selectedProject} not found`); - } - - const dir = selected.getModulePath()!; - log.success(`Deploying project ${selectedProject} from ${dir} to database ${database}...`); + projectName = await selectModule(argv, prompter, 'Choose a project to deploy', cwd); + log.info(`Selected project: ${projectName}`); + } - if (argv.fast) { - await deployFast({ - opts: options, - database, - dir, - name: selectedProject, - usePlan: true, - cache: false - }); - } else { - await deploy(options, selectedProject, database, dir, { useSqitch, useTransaction: tx }); - } + await deployModules({ + database, + cwd, + recursive, + projectName, + useSqitch, + useTransaction: tx, + fast: argv.fast, + usePlan: argv.usePlan ?? true, + cache: argv.cache ?? false + }); - log.success('Deployment complete.'); - } else { - if (useSqitch) { - log.info(`Running: sqitch deploy db:pg:${database} (using legacy Sqitch)`); - execSync(`sqitch deploy db:pg:${database}`, { - cwd, - env: getSpawnEnvWithPg(pgEnv), - stdio: 'inherit' - }); - } else { - log.info(`Running: launchql migrate deploy db:pg:${database}`); - await deployCommand(pgEnv, database, cwd, { useTransaction: tx }); - } - log.success('Deployment complete.'); - } + log.success('Deployment complete.'); return argv; }; diff --git a/packages/cli/src/commands/revert.ts b/packages/cli/src/commands/revert.ts index 37a1f3e5f..51251c8d4 100644 --- a/packages/cli/src/commands/revert.ts +++ b/packages/cli/src/commands/revert.ts @@ -1,11 +1,8 @@ import { CLIOptions, Inquirerer, Question } from 'inquirerer'; -import { LaunchQLProject, revert } from '@launchql/core'; -import { errors, getEnvOptions, LaunchQLOptions } from '@launchql/types'; -import { getPgEnvOptions, getSpawnEnvWithPg } from 'pg-env'; import { Logger } from '@launchql/logger'; -import { revertCommand } from '@launchql/migrate'; -import { execSync } from 'child_process'; +import { revertModules } from '@launchql/core'; import { getTargetDatabase } from '../utils'; +import { selectModule } from '../utils/module-utils'; const log = new Logger('revert'); @@ -45,52 +42,22 @@ export default async ( log.debug(`Using current directory: ${cwd}`); - const project = new LaunchQLProject(cwd); - + let projectName: string | undefined; if (recursive) { - const modules = await project.getModules(); - const moduleNames = modules.map(mod => mod.getModuleName()); - - if (!moduleNames.length) { - log.error('No modules found in the specified directory.'); - prompter.close(); - throw errors.NOT_FOUND({}, 'No modules found in the specified directory.'); - } - - const { project: selectedProject } = await prompter.prompt(argv, [ - { - type: 'autocomplete', - name: 'project', - message: 'Choose a project to revert', - options: moduleNames, - required: true - } - ]); + projectName = await selectModule(argv, prompter, 'Choose a project to revert', cwd); + log.info(`Selected project: ${projectName}`); + } - log.success(`Reverting project ${selectedProject} on database ${database}...`); - const options: LaunchQLOptions = getEnvOptions({ - pg: { - database - } - }); + await revertModules({ + database, + cwd, + recursive, + projectName, + useSqitch, + useTransaction: tx + }); - await revert(options, selectedProject, database, cwd, { useSqitch, useTransaction: tx }); - log.success('Revert complete.'); - } else { - const pgEnv = getPgEnvOptions(); - if (useSqitch) { - log.info(`Running: sqitch revert db:pg:${database} (using legacy Sqitch)`); - execSync(`sqitch revert db:pg:${database}`, { - cwd, - env: getSpawnEnvWithPg(pgEnv), - stdio: 'inherit' - }); - } else { - log.info(`Running: launchql migrate revert db:pg:${database}`); - await revertCommand(pgEnv, database, cwd, { useTransaction: tx }); - } - log.success('Revert complete.'); - } + log.success('Revert complete.'); return argv; }; diff --git a/packages/cli/src/commands/verify.ts b/packages/cli/src/commands/verify.ts index d404c640b..28c7f9ce9 100644 --- a/packages/cli/src/commands/verify.ts +++ b/packages/cli/src/commands/verify.ts @@ -1,11 +1,8 @@ import { CLIOptions, Inquirerer, Question } from 'inquirerer'; -import { LaunchQLProject, verify } from '@launchql/core'; -import { errors, getEnvOptions, LaunchQLOptions } from '@launchql/types'; -import { getPgEnvOptions, getSpawnEnvWithPg } from 'pg-env'; import { Logger } from '@launchql/logger'; -import { verifyCommand } from '@launchql/migrate'; -import { execSync } from 'child_process'; +import { verifyModules } from '@launchql/core'; import { getTargetDatabase } from '../utils'; +import { selectModule } from '../utils/module-utils'; const log = new Logger('verify'); @@ -24,52 +21,21 @@ export default async ( log.debug(`Using current directory: ${cwd}`); - const project = new LaunchQLProject(cwd); - + let projectName: string | undefined; if (recursive) { - const modules = await project.getModules(); - const moduleNames = modules.map(mod => mod.getModuleName()); - - if (!moduleNames.length) { - log.error('No modules found in the specified directory.'); - prompter.close(); - throw errors.NOT_FOUND({}, 'No modules found in the specified directory.'); - } - - const { project: selectedProject } = await prompter.prompt(argv, [ - { - type: 'autocomplete', - name: 'project', - message: 'Choose a project to verify', - options: moduleNames, - required: true - } - ]); + projectName = await selectModule(argv, prompter, 'Choose a project to verify', cwd); + log.info(`Selected project: ${projectName}`); + } - const options: LaunchQLOptions = getEnvOptions({ - pg: { - database - } - }); + await verifyModules({ + database, + cwd, + recursive, + projectName, + useSqitch + }); - log.info(`Verifying project ${selectedProject} on database ${database}...`); - await verify(options, selectedProject, database, cwd, { useSqitch }); - log.success('Verify complete.'); - } else { - const pgEnv = getPgEnvOptions(); - if (useSqitch) { - log.info(`Running: sqitch verify db:pg:${database} (using legacy Sqitch)`); - execSync(`sqitch verify db:pg:${database}`, { - cwd, - env: getSpawnEnvWithPg(pgEnv), - stdio: 'inherit' - }); - } else { - log.info(`Running: launchql migrate verify db:pg:${database}`); - await verifyCommand(pgEnv, database, cwd); - } - log.success('Verify complete.'); - } + log.success('Verify complete.'); return argv; }; diff --git a/packages/cli/src/utils/module-utils.ts b/packages/cli/src/utils/module-utils.ts new file mode 100644 index 000000000..da9a6c263 --- /dev/null +++ b/packages/cli/src/utils/module-utils.ts @@ -0,0 +1,31 @@ +import { Inquirerer } from 'inquirerer'; +import { ParsedArgs } from 'minimist'; +import { getAvailableModules } from '@launchql/core'; +import { errors } from '@launchql/types'; + +/** + * Prompt user to select a module from available modules in the directory + */ +export async function selectModule( + argv: Partial, + prompter: Inquirerer, + message: string, + cwd: string +): Promise { + const modules = await getAvailableModules(cwd); + + if (!modules.length) { + prompter.close(); + throw errors.NOT_FOUND({}, 'No modules found in the specified directory.'); + } + + const { project } = await prompter.prompt(argv, [{ + type: 'autocomplete', + name: 'project', + message, + options: modules, + required: true + }]); + + return project; +} \ No newline at end of file diff --git a/packages/core/src/deploy-fast.ts b/packages/core/src/deploy-fast.ts deleted file mode 100644 index 7b165dedb..000000000 --- a/packages/core/src/deploy-fast.ts +++ /dev/null @@ -1,119 +0,0 @@ -// FASTER than deploy-stream -// Time: 1.056 s -import { resolve } from 'path'; - -import { LaunchQLOptions, errors } from '@launchql/types'; -import { PgConfig } from 'pg-env'; -import { Logger } from '@launchql/logger'; -import { getPgPool } from 'pg-cache'; -import { LaunchQLProject } from './class/launchql'; -import { packageModule } from './package'; - -interface Extensions { - resolved: string[]; - external: string[]; -} - -interface DeployFastOptions { - opts: LaunchQLOptions; - name: string; - database: string; - dir: string; - usePlan: boolean; - cache?: boolean; -} - -const deployFastCache: Record>> = {}; - -const getCacheKey = ( - pg: Partial | undefined, - name: string, - database: string -): string => { - const { host, port, user } = pg ?? {}; - return `${host ?? 'localhost'}:${port ?? 5432}:${user ?? 'user'}:${database}:${name}`; -}; - -export const deployFast = async ( - options: DeployFastOptions -): Promise => { - const { - dir, - name, - database, - opts, - usePlan, - cache = false - } = options; - - const log = new Logger('deploy-fast'); - - const projectRoot = new LaunchQLProject(dir); - const modules = projectRoot.getModuleMap(); - - log.info(`🔍 Gathering modules from ${dir}...`); - - if (!modules[name]) { - log.error(`❌ Module "${name}" not found.`); - throw new Error(`Module "${name}" does not exist.`); - } - - log.info(`📦 Resolving dependencies for ${name}...`); - const extensions: Extensions = projectRoot.getModuleExtensions(); - - const pgPool = getPgPool({ ...opts.pg, database }); - - log.success(`🚀 Deploying to database: ${database}`); - - for (const extension of extensions.resolved) { - try { - if (extensions.external.includes(extension)) { - const query = `CREATE EXTENSION IF NOT EXISTS "${extension}" CASCADE;`; - log.info(`📥 Installing external extension: ${extension}`); - log.debug(`> ${query}`); - await pgPool.query(query); - } else { - const modulePath = resolve(projectRoot.workspacePath, modules[extension].path); - const localProject = new LaunchQLProject(modulePath); - - const cacheKey = getCacheKey(opts.pg, extension, database); - if (cache && deployFastCache[cacheKey]) { - log.warn(`⚡ Using cached pkg for ${extension}.`); - await pgPool.query(deployFastCache[cacheKey].sql); - continue; - } - - let pkg; - try { - pkg = await packageModule(localProject.modulePath, { usePlan, extension: false }); - } catch (err) { - log.error(`❌ Failed to package module "${extension}" at path: ${modulePath}`); - log.error(` Error: ${err instanceof Error ? err.message : String(err)}`); - console.error(err); // Preserve full stack trace - throw errors.DEPLOYMENT_FAILED({ - type: 'Deployment', - module: extension - }); - } - - log.info(`📂 Deploying local module: ${extension}`); - log.debug(`→ Path: ${modulePath}`); - log.debug(`→ Command: sqitch deploy db:pg:${database}`); - log.debug(`> ${pkg.sql}`); - - await pgPool.query(pkg.sql); - - if (cache) { - deployFastCache[cacheKey] = pkg; - } - } - } catch (err) { - log.error(`🛑 Deployment error: ${err instanceof Error ? err.message : err}`); - console.error(err); // Preserve stack trace - throw errors.DEPLOYMENT_FAILED({ type: 'Deployment', module: extension }); - } - } - - log.success(`✅ Deployment complete for module: ${name}`); - return extensions; -}; diff --git a/packages/core/src/deploy-stream.ts b/packages/core/src/deploy-stream.ts deleted file mode 100644 index bcf1b953c..000000000 --- a/packages/core/src/deploy-stream.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { resolve } from 'path'; - -import { errors, LaunchQLOptions } from '@launchql/types'; -import { Logger } from '@launchql/logger'; -import { getPgPool } from 'pg-cache'; -import { LaunchQLProject } from './class/launchql'; -import { packageModule } from './package'; -import { streamSql } from './stream-sql'; - -interface Extensions { - resolved: string[]; - external: string[]; -} - -interface DeployFastOptions { - opts: LaunchQLOptions; - name: string; - database: string; - dir: string; - usePlan: boolean; -} - -export const deployStream = async ( - options: DeployFastOptions -): Promise => { - const { - dir, - name, - database, - opts, - usePlan - } = options; - - const log = new Logger('deploy-stream'); - - const projectRoot = new LaunchQLProject(dir); - const modules = projectRoot.getModuleMap(); - - log.info(`🔍 Gathering modules from ${dir}...`); - - if (!modules[name]) { - log.error(`❌ Module "${name}" not found.`); - throw new Error(`Module "${name}" does not exist.`); - } - - log.info(`📦 Resolving dependencies for ${name}...`); - const extensions: Extensions = projectRoot.getModuleExtensions(); - - const pgPool = getPgPool({ ...opts.pg, database }); - - log.success(`🚀 Deploying to database: ${database}`); - - for (const extension of extensions.resolved) { - try { - if (extensions.external.includes(extension)) { - const query = `CREATE EXTENSION IF NOT EXISTS "${extension}" CASCADE;`; - log.info(`📥 Installing external extension: ${extension}`); - log.debug(`> ${query}`); - await pgPool.query(query); - } else { - const modulePath = resolve(projectRoot.workspacePath, modules[extension].path); - const localProject = new LaunchQLProject(modulePath); - const pkg = await packageModule(localProject.modulePath, { usePlan, extension: false }); - - log.info(`📂 Deploying local module: ${extension}`); - log.debug(`→ Path: ${modulePath}`); - log.debug(`→ Command: sqitch deploy db:pg:${database}`); - log.debug(`> ${pkg.sql}`); - - await streamSql({ - database, - host: opts.pg.host, - user: opts.pg.user, - password: opts.pg.password, - port: opts.pg.port - }, pkg.sql); - } - } catch (err) { - log.error(`🛑 Deployment error: ${err instanceof Error ? err.message : err}`); - console.error(err); // keep stack trace - throw errors.DEPLOYMENT_FAILED({ type: 'Deployment', module: extension }); - } - } - - log.success(`✅ Deployment complete for module: ${name}`); - return extensions; -}; diff --git a/packages/core/src/export-migrations.ts b/packages/core/src/export-migrations.ts index dbdc7d72a..0d68a0234 100644 --- a/packages/core/src/export-migrations.ts +++ b/packages/core/src/export-migrations.ts @@ -6,7 +6,7 @@ import Case from 'case'; import { exportMeta } from './export-meta'; import { getPgPool } from 'pg-cache'; import { LaunchQLOptions } from '@launchql/types'; -import { SqitchRow, writeSqitchFiles, writeSqitchPlan } from './sqitch/utils'; +import { SqitchRow, writeSqitchFiles, writeSqitchPlan } from './projects/utils'; import { LaunchQLProject } from './class/launchql'; interface ExportMigrationsToDiskOptions { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b593b87bf..1686e0d03 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,6 +1,4 @@ export * from './class/launchql'; -export * from './deploy-fast'; -export * from './deploy-stream'; export * from './deps'; export * from './export-meta'; export * from './export-migrations'; @@ -9,8 +7,15 @@ export * from './modules'; export * from './package'; export * from './paths'; export * from './resolve'; -export * from './sqitch/deploy'; -export * from './sqitch/revert'; -export * from './sqitch/verify'; +export * from './projects/deploy-project'; +export * from './projects/revert-project'; +export * from './projects/verify-project'; export * from './transform'; -export * from './utils'; \ No newline at end of file +export * from './utils'; + +// New exports for migration API +export * from './migrate/migration'; +export { runSqitch } from './utils/sqitch-wrapper'; +export { deployModule } from './migrate/deploy-module'; +export { revertModule } from './migrate/revert-module'; +export { verifyModule } from './migrate/verify-module'; \ No newline at end of file diff --git a/packages/migrate/src/commands/deploy.ts b/packages/core/src/migrate/deploy-module.ts similarity index 91% rename from packages/migrate/src/commands/deploy.ts rename to packages/core/src/migrate/deploy-module.ts index a2b66f6ae..dcdcc7af8 100644 --- a/packages/migrate/src/commands/deploy.ts +++ b/packages/core/src/migrate/deploy-module.ts @@ -1,7 +1,7 @@ import { join } from 'path'; import { existsSync } from 'fs'; -import { LaunchQLMigrate } from '../client'; -import { MigrateConfig } from '../types'; +import { LaunchQLMigrate } from '@launchql/migrate'; +import { MigrateConfig } from '@launchql/migrate'; import { Logger } from '@launchql/logger'; const log = new Logger('migrate-deploy'); @@ -10,7 +10,7 @@ const log = new Logger('migrate-deploy'); * Deploy command that mimics sqitch deploy behavior * This is designed to be a drop-in replacement for spawn('sqitch', ['deploy', 'db:pg:database']) */ -export async function deployCommand( +export async function deployModule( config: Partial, database: string, cwd: string, diff --git a/packages/core/src/migrate/migration.ts b/packages/core/src/migrate/migration.ts new file mode 100644 index 000000000..57c4fbd0f --- /dev/null +++ b/packages/core/src/migrate/migration.ts @@ -0,0 +1,174 @@ +import { LaunchQLProject } from '../class/launchql'; +import { deployProject } from '../projects/deploy-project'; +import { revertProject } from '../projects/revert-project'; +import { verifyProject } from '../projects/verify-project'; +import { getEnvOptions } from '@launchql/types'; +import { getPgEnvOptions } from 'pg-env'; +import { Logger } from '@launchql/logger'; +import { deployModule } from './deploy-module'; +import { revertModule } from './revert-module'; +import { verifyModule } from './verify-module'; +import { runSqitch } from '../utils/sqitch-wrapper'; + +const log = new Logger('project-commands'); + +export interface MigrationOptions { + database: string; + cwd: string; + recursive?: boolean; + projectName?: string; // Required if recursive=true + useSqitch?: boolean; + useTransaction?: boolean; + toChange?: string; + // Options for fast deployment + fast?: boolean; + usePlan?: boolean; + cache?: boolean; +} + +/** + * Deploy a project - handles both recursive (multi-module) and non-recursive (single directory) deployments + */ +export async function deployModules(options: MigrationOptions): Promise { + if (options.recursive) { + if (!options.projectName) { + throw new Error('projectName is required when recursive is true'); + } + + // Use existing deploy() from core that handles dependencies + const project = new LaunchQLProject(options.cwd); + const modules = project.getModuleMap(); + + if (!modules[options.projectName]) { + throw new Error(`Module "${options.projectName}" not found`); + } + + const modulePath = modules[options.projectName].path; + log.info(`Deploying project ${options.projectName} from ${modulePath} to database ${options.database}...`); + + await deployProject( + getEnvOptions({ pg: { database: options.database } }), + options.projectName, + options.database, + modulePath, + { + useSqitch: options.useSqitch, + useTransaction: options.useTransaction, + fast: options.fast, + usePlan: options.usePlan, + cache: options.cache + } + ); + } else { + // Direct execution on current directory + if (options.useSqitch) { + await runSqitch('deploy', options.database, options.cwd); + } else { + await deployModule( + getPgEnvOptions(), + options.database, + options.cwd, + { + useTransaction: options.useTransaction, + toChange: options.toChange + } + ); + } + } +} + +/** + * Revert a project - handles both recursive (multi-module) and non-recursive (single directory) reverts + */ +export async function revertModules(options: MigrationOptions): Promise { + if (options.recursive) { + if (!options.projectName) { + throw new Error('projectName is required when recursive is true'); + } + + // Use existing revert() from core + const project = new LaunchQLProject(options.cwd); + const modules = project.getModuleMap(); + + if (!modules[options.projectName]) { + throw new Error(`Module "${options.projectName}" not found`); + } + + log.info(`Reverting project ${options.projectName} on database ${options.database}...`); + + await revertProject( + getEnvOptions({ pg: { database: options.database } }), + options.projectName, + options.database, + options.cwd, + { + useSqitch: options.useSqitch, + useTransaction: options.useTransaction + } + ); + } else { + // Direct execution on current directory + if (options.useSqitch) { + await runSqitch('revert', options.database, options.cwd); + } else { + await revertModule( + getPgEnvOptions(), + options.database, + options.cwd, + { + useTransaction: options.useTransaction, + toChange: options.toChange + } + ); + } + } +} + +/** + * Verify a project - handles both recursive (multi-module) and non-recursive (single directory) verification + */ +export async function verifyModules(options: MigrationOptions): Promise { + if (options.recursive) { + if (!options.projectName) { + throw new Error('projectName is required when recursive is true'); + } + + // Use existing verify() from core + const project = new LaunchQLProject(options.cwd); + const modules = project.getModuleMap(); + + if (!modules[options.projectName]) { + throw new Error(`Module "${options.projectName}" not found`); + } + + log.info(`Verifying project ${options.projectName} on database ${options.database}...`); + + await verifyProject( + getEnvOptions({ pg: { database: options.database } }), + options.projectName, + options.database, + options.cwd, + { useSqitch: options.useSqitch } + ); + } else { + // Direct execution on current directory + if (options.useSqitch) { + await runSqitch('verify', options.database, options.cwd); + } else { + await verifyModule( + getPgEnvOptions(), + options.database, + options.cwd + ); + } + } +} + +/** + * Get available modules in a directory + */ +export async function getAvailableModules(cwd: string): Promise { + const project = new LaunchQLProject(cwd); + const modules = await project.getModules(); + return modules.map(mod => mod.getModuleName()); +} \ No newline at end of file diff --git a/packages/migrate/src/commands/revert.ts b/packages/core/src/migrate/revert-module.ts similarity index 81% rename from packages/migrate/src/commands/revert.ts rename to packages/core/src/migrate/revert-module.ts index 7e0ad1776..315928605 100644 --- a/packages/migrate/src/commands/revert.ts +++ b/packages/core/src/migrate/revert-module.ts @@ -1,7 +1,7 @@ import { join } from 'path'; import { existsSync } from 'fs'; -import { LaunchQLMigrate } from '../client'; -import { MigrateConfig } from '../types'; +import { LaunchQLMigrate } from '@launchql/migrate'; +import { MigrateConfig } from '@launchql/migrate'; import { Logger } from '@launchql/logger'; const log = new Logger('migrate-revert'); @@ -10,7 +10,7 @@ const log = new Logger('migrate-revert'); * Revert command that mimics sqitch revert behavior * This is designed to be a drop-in replacement for spawn('sqitch', ['revert', 'db:pg:database']) */ -export async function revertCommand( +export async function revertModule( config: Partial, database: string, cwd: string, @@ -27,11 +27,11 @@ export async function revertCommand( // Provide defaults for missing config values const fullConfig: MigrateConfig = { - host: config.host || 'localhost', - port: config.port || 5432, - user: config.user || 'postgres', - password: config.password || '', - database: config.database || 'postgres' + host: config.host, + port: config.port, + user: config.user, + password: config.password, + database: config.database }; const client = new LaunchQLMigrate(fullConfig); diff --git a/packages/migrate/src/commands/verify.ts b/packages/core/src/migrate/verify-module.ts similarity index 79% rename from packages/migrate/src/commands/verify.ts rename to packages/core/src/migrate/verify-module.ts index 1d02ce355..6892c94e8 100644 --- a/packages/migrate/src/commands/verify.ts +++ b/packages/core/src/migrate/verify-module.ts @@ -1,7 +1,7 @@ import { join } from 'path'; import { existsSync } from 'fs'; -import { LaunchQLMigrate } from '../client'; -import { MigrateConfig } from '../types'; +import { LaunchQLMigrate } from '@launchql/migrate'; +import { MigrateConfig } from '@launchql/migrate'; import { Logger } from '@launchql/logger'; const log = new Logger('migrate-verify'); @@ -10,7 +10,7 @@ const log = new Logger('migrate-verify'); * Verify command that mimics sqitch verify behavior * This is designed to be a drop-in replacement for spawn('sqitch', ['verify', 'db:pg:database']) */ -export async function verifyCommand( +export async function verifyModule( config: Partial, database: string, cwd: string @@ -25,11 +25,11 @@ export async function verifyCommand( // Provide defaults for missing config values const fullConfig: MigrateConfig = { - host: config.host || 'localhost', - port: config.port || 5432, - user: config.user || 'postgres', - password: config.password || '', - database: config.database || 'postgres' + host: config.host, + port: config.port, + user: config.user, + password: config.password, + database: config.database }; const client = new LaunchQLMigrate(fullConfig); diff --git a/packages/core/src/sqitch/deploy.ts b/packages/core/src/projects/deploy-project.ts similarity index 57% rename from packages/core/src/sqitch/deploy.ts rename to packages/core/src/projects/deploy-project.ts index 1c76649c7..6efd12d09 100644 --- a/packages/core/src/sqitch/deploy.ts +++ b/packages/core/src/projects/deploy-project.ts @@ -2,25 +2,55 @@ import { resolve } from 'path'; import { spawn } from 'child_process'; import { errors, LaunchQLOptions } from '@launchql/types'; -import { getSpawnEnvWithPg } from 'pg-env'; +import { getSpawnEnvWithPg, PgConfig } from 'pg-env'; import { Logger } from '@launchql/logger'; import { getPgPool } from 'pg-cache'; -import { deployCommand } from '@launchql/migrate'; +import { deployModule } from '../migrate/deploy-module'; import { LaunchQLProject } from '../class/launchql'; +import { packageModule } from '../package'; interface Extensions { resolved: string[]; external: string[]; } +// Cache for fast deployment +const deployFastCache: Record>> = {}; + +const getCacheKey = ( + pg: PgConfig, + name: string, + database: string +): string => { + const { host, port, user } = pg ?? {}; + return `${host}:${port}:${user}:${database}:${name}`; +}; + const log = new Logger('deploy'); -export const deploy = async ( +export const deployProject = async ( opts: LaunchQLOptions, name: string, database: string, dir: string, - options?: { useSqitch?: boolean; useTransaction?: boolean } + options?: { + useSqitch?: boolean; + useTransaction?: boolean; + /** + * If true, use the fast deployment strategy + * This will skip the sqitch deployment and new migration system and simply deploy the packaged sql + * Defaults to true for launchql + */ + fast?: boolean; + /** + * if fast is true, you can choose to use the plan file or simply leverage the dependencies + */ + usePlan?: boolean; + /** + * if fast is true, you can choose to cache the packaged module + */ + cache?: boolean; + } ): Promise => { const mod = new LaunchQLProject(dir); @@ -35,10 +65,7 @@ export const deploy = async ( log.info(`📦 Resolving dependencies for ${name}...`); const extensions: Extensions = mod.getModuleExtensions(); - const pgPool = getPgPool({ - ...opts.pg, - database - }); + const pgPool = getPgPool({ ...opts.pg, database }); log.success(`🚀 Starting deployment to database ${database}...`); @@ -54,7 +81,42 @@ export const deploy = async ( log.info(`📂 Deploying local module: ${extension}`); log.debug(`→ Path: ${modulePath}`); - if (options?.useSqitch) { + if (options?.fast ?? true) { + // Use fast deployment strategy + const localProject = new LaunchQLProject(modulePath); + const cacheKey = getCacheKey(opts.pg as PgConfig, extension, database); + + if (options?.cache && deployFastCache[cacheKey]) { + log.warn(`⚡ Using cached pkg for ${extension}.`); + await pgPool.query(deployFastCache[cacheKey].sql); + continue; + } + + let pkg; + try { + pkg = await packageModule(localProject.modulePath, { + usePlan: options?.usePlan ?? true, + extension: false + }); + } catch (err) { + log.error(`❌ Failed to package module "${extension}" at path: ${modulePath}`); + log.error(` Error: ${err instanceof Error ? err.message : String(err)}`); + console.error(err); // Preserve full stack trace + throw errors.DEPLOYMENT_FAILED({ + type: 'Deployment', + module: extension + }); + } + + log.debug(`→ Command: sqitch deploy db:pg:${database}`); + log.debug(`> ${pkg.sql}`); + + await pgPool.query(pkg.sql); + + if (options?.cache) { + deployFastCache[cacheKey] = pkg; + } + } else if (options?.useSqitch) { // Use legacy sqitch log.debug(`→ Command: sqitch deploy db:pg:${database}`); @@ -93,7 +155,7 @@ export const deploy = async ( log.debug(`→ Command: launchql migrate deploy db:pg:${database}`); try { - await deployCommand(opts.pg, database, modulePath, { useTransaction: options?.useTransaction }); + await deployModule(opts.pg, database, modulePath, { useTransaction: options?.useTransaction }); } catch (deployError) { log.error(`❌ Deployment failed for module ${extension}`); throw errors.DEPLOYMENT_FAILED({ type: 'Deployment', module: extension }); diff --git a/packages/core/src/sqitch/revert.ts b/packages/core/src/projects/revert-project.ts similarity index 94% rename from packages/core/src/sqitch/revert.ts rename to packages/core/src/projects/revert-project.ts index 5422032d3..80384fa54 100644 --- a/packages/core/src/sqitch/revert.ts +++ b/packages/core/src/projects/revert-project.ts @@ -6,7 +6,7 @@ import { errors, LaunchQLOptions } from '@launchql/types'; import { getSpawnEnvWithPg } from 'pg-env'; import { Logger } from '@launchql/logger'; import { getPgPool } from 'pg-cache'; -import { revertCommand } from '@launchql/migrate'; +import { revertModule } from '../migrate/revert-module'; interface Extensions { resolved: string[]; @@ -15,7 +15,7 @@ interface Extensions { const log = new Logger('revert'); -export const revert = async ( +export const revertProject = async ( opts: LaunchQLOptions, name: string, database: string, @@ -94,7 +94,7 @@ export const revert = async ( log.debug(`→ Command: launchql migrate revert db:pg:${database}`); try { - await revertCommand(opts.pg, database, modulePath, { useTransaction: options?.useTransaction }); + await revertModule(opts.pg, database, modulePath, { useTransaction: options?.useTransaction }); } catch (revertError) { log.error(`❌ Revert failed for module ${extension}`); throw errors.DEPLOYMENT_FAILED({ type: 'Revert', module: extension }); diff --git a/packages/core/src/sqitch/utils.ts b/packages/core/src/projects/utils.ts similarity index 100% rename from packages/core/src/sqitch/utils.ts rename to packages/core/src/projects/utils.ts diff --git a/packages/core/src/sqitch/verify.ts b/packages/core/src/projects/verify-project.ts similarity index 95% rename from packages/core/src/sqitch/verify.ts rename to packages/core/src/projects/verify-project.ts index 0f6cc5cab..928c16a07 100644 --- a/packages/core/src/sqitch/verify.ts +++ b/packages/core/src/projects/verify-project.ts @@ -6,7 +6,7 @@ import { getSpawnEnvWithPg } from 'pg-env'; import { LaunchQLProject } from '../class/launchql'; import { Logger } from '@launchql/logger'; import { getPgPool } from 'pg-cache'; -import { verifyCommand } from '@launchql/migrate'; +import { verifyModule } from '../migrate/verify-module'; interface Extensions { resolved: string[]; @@ -15,7 +15,7 @@ interface Extensions { const log = new Logger('verify'); -export const verify = async ( +export const verifyProject = async ( opts: LaunchQLOptions, name: string, database: string, @@ -83,7 +83,7 @@ export const verify = async ( }); } else { // Use new migration system - await verifyCommand(opts.pg, database, modulePath); + await verifyModule(opts.pg, database, modulePath); } } catch (verifyError) { log.error(`❌ Verification failed for module ${extension}`); diff --git a/packages/core/src/stream-sql.ts b/packages/core/src/stream-sql.ts deleted file mode 100644 index 467bc56ca..000000000 --- a/packages/core/src/stream-sql.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { getSpawnEnvWithPg } from 'pg-env'; -import { spawn } from 'child_process'; -import { Readable } from 'stream'; - -interface PgConfig { - user: string; - host: string; - database: string; - password: string; - port?: number; -} - -function setArgs(config: PgConfig): string[] { - const args = [ - '-U', config.user, - '-h', config.host, - '-d', config.database - ]; - if (config.port) { - args.push('-p', String(config.port)); - } - return args; -} - -// Converts a string to a readable stream (replaces streamify-string) -function stringToStream(text: string): Readable { - const stream = new Readable({ - read() { - this.push(text); - this.push(null); - } - }); - return stream; -} - -export async function streamSql(config: PgConfig, sql: string): Promise { - const args = setArgs(config); - - return new Promise((resolve, reject) => { - const sqlStream = stringToStream(sql); - - const proc = spawn('psql', args, { - env: getSpawnEnvWithPg(config) - }); - - sqlStream.pipe(proc.stdin); - - proc.on('close', (code) => { - resolve(); - }); - - proc.on('error', (error) => { - reject(error); - }); - - proc.stderr.on('data', (data: Buffer) => { - reject(new Error(data.toString())); - }); - }); -} diff --git a/packages/core/src/utils/sqitch-wrapper.ts b/packages/core/src/utils/sqitch-wrapper.ts new file mode 100644 index 000000000..98ab0a2c9 --- /dev/null +++ b/packages/core/src/utils/sqitch-wrapper.ts @@ -0,0 +1,25 @@ +import { execSync } from 'child_process'; +import { getSpawnEnvWithPg, getPgEnvOptions } from 'pg-env'; +import { Logger } from '@launchql/logger'; + +const log = new Logger('sqitch-wrapper'); + +/** + * Wrapper for executing sqitch commands + */ +export async function runSqitch( + command: 'deploy' | 'revert' | 'verify', + database: string, + cwd: string +): Promise { + const pgEnv = getPgEnvOptions(); + const cmd = `sqitch ${command} db:pg:${database}`; + + log.info(`Running: ${cmd}`); + + execSync(cmd, { + cwd, + env: getSpawnEnvWithPg(pgEnv), + stdio: 'inherit' + }); +} \ No newline at end of file diff --git a/packages/migrate/src/index.ts b/packages/migrate/src/index.ts index 46f091367..09ada5e7b 100644 --- a/packages/migrate/src/index.ts +++ b/packages/migrate/src/index.ts @@ -3,7 +3,4 @@ export * from './types'; export { parsePlanFile, getChangeNamesFromPlan, getChangesInOrder } from './parser/plan'; export { hashFile, hashString } from './utils/hash'; export { readScript, scriptExists } from './utils/fs'; -export { withTransaction, TransactionContext, TransactionOptions } from './utils/transaction'; -export { deployCommand } from './commands/deploy'; -export { revertCommand } from './commands/revert'; -export { verifyCommand } from './commands/verify'; \ No newline at end of file +export { withTransaction, TransactionContext, TransactionOptions } from './utils/transaction'; \ No newline at end of file diff --git a/packages/pgsql-test/src/seed/launchql.ts b/packages/pgsql-test/src/seed/launchql.ts index 380ff728c..187546337 100644 --- a/packages/pgsql-test/src/seed/launchql.ts +++ b/packages/pgsql-test/src/seed/launchql.ts @@ -1,6 +1,6 @@ import { SeedAdapter, SeedContext } from './types'; import { getEnvOptions } from '@launchql/types'; -import { LaunchQLProject, deployFast } from '@launchql/core'; +import { LaunchQLProject, deployProject } from '@launchql/core'; export function launchql(cwd?: string, cache: boolean = false): SeedAdapter { return { @@ -10,14 +10,11 @@ export function launchql(cwd?: string, cache: boolean = false): SeedAdapter { const opts = getEnvOptions({ pg: ctx.config }); - await deployFast({ - opts, - name: proj.getModuleName(), - database: ctx.config.database, - dir: proj.modulePath, - usePlan: true, - cache - }); + await deployProject(opts, proj.getModuleName(), ctx.config.database, proj.modulePath, { + fast: true, + usePlan: true, + cache + }); } }; } diff --git a/packages/pgsql-test/src/seed/sqitch.ts b/packages/pgsql-test/src/seed/sqitch.ts index b56e2f3a9..9e6a29d58 100644 --- a/packages/pgsql-test/src/seed/sqitch.ts +++ b/packages/pgsql-test/src/seed/sqitch.ts @@ -1,14 +1,17 @@ import { SeedAdapter, SeedContext } from './types'; import { getEnvOptions } from '@launchql/types'; -import { LaunchQLProject, deploy } from '@launchql/core'; +import { LaunchQLProject, deployProject } from '@launchql/core'; export function sqitch(cwd?: string): SeedAdapter { return { async seed(ctx: SeedContext) { - const proj = new LaunchQLProject(cwd ?? ctx.connect.cwd); + const proj = new LaunchQLProject(cwd ?? ctx.connect.cwd); if (!proj.isInModule()) return; const opts = getEnvOptions({ pg: ctx.config }); - await deploy(opts, proj.getModuleName(), ctx.config.database, proj.modulePath); + await deployProject(opts, proj.getModuleName(), ctx.config.database, proj.modulePath, { + useSqitch: true, + fast: false + }); } }; }