Skip to content

Commit

Permalink
feat(migrate diff): add --exit-code (#12227)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jolg42 committed Mar 10, 2022
1 parent e0b3b76 commit f392edc
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 54 deletions.
7 changes: 6 additions & 1 deletion packages/migrate/src/__tests__/MigrateDev.test.ts
Expand Up @@ -1337,7 +1337,12 @@ describe('mysql', () => {
})

describeIf(!process.env.TEST_SKIP_MSSQL)('SQL Server', () => {
jest.setTimeout(20000)
if (process.env.CI) {
// to avoid timeouts on macOS
jest.setTimeout(80_000)
} else {
jest.setTimeout(20_000)
}

const connectionString = process.env.TEST_MSSQL_URI || 'mssql://SA:Pr1sm4_Pr1sm4@localhost:1433/master'

Expand Down
119 changes: 102 additions & 17 deletions packages/migrate/src/__tests__/MigrateDiff.test.ts
Expand Up @@ -101,10 +101,26 @@ describe('migrate diff', () => {
it('should diff --from-url=file:doesnotexists.db --to-empty ', async () => {
ctx.fixture('schema-only-sqlite')

const result = MigrateDiff.new().parse(['--preview-feature', '--from-empty', '--to-url=file:doesnotexists.db'])
const result = MigrateDiff.new().parse(['--preview-feature', '--from-url=file:doesnotexists.db', '--to-empty'])
await expect(result).resolves.toMatchInlineSnapshot(``)
expect(ctx.mocked['console.info'].mock.calls.join('\n')).toMatchInlineSnapshot(`No difference detected.`)
})
it('should fail if path does not exist', async () => {
ctx.fixture('schema-only-sqlite')

const result = MigrateDiff.new().parse([
'--preview-feature',
'--from-url=file:./something/doesnotexists.db',
'--to-empty',
])
await expect(result).rejects.toMatchInlineSnapshot(`
unable to open database file: ./something/doesnotexists.db
`)
expect(ctx.mocked['console.error'].mock.calls.join('\n')).toMatchInlineSnapshot(``)
expect(ctx.mocked['console.info'].mock.calls.join('\n')).toMatchInlineSnapshot(``)
})

it('should diff --from-empty --to-url=file:dev.db', async () => {
ctx.fixture('introspection/sqlite')
Expand All @@ -113,18 +129,18 @@ describe('migrate diff', () => {
await expect(result).resolves.toMatchInlineSnapshot(``)
expect(ctx.mocked['console.info'].mock.calls.join('\n')).toMatchInlineSnapshot(`
[+] Added tables
- Post
- Profile
- User
- _Migration
[+] Added tables
- Post
- Profile
- User
- _Migration
[*] Changed the \`Profile\` table
[+] Added unique index on columns (userId)
[*] Changed the \`Profile\` table
[+] Added unique index on columns (userId)
[*] Changed the \`User\` table
[+] Added unique index on columns (email)
`)
[*] Changed the \`User\` table
[+] Added unique index on columns (email)
`)
})
it('should diff --from-empty --to-url=file:dev.db --script', async () => {
ctx.fixture('introspection/sqlite')
Expand All @@ -145,9 +161,9 @@ describe('migrate diff', () => {
await expect(result).resolves.toMatchInlineSnapshot(``)
expect(ctx.mocked['console.info'].mock.calls.join('\n')).toMatchInlineSnapshot(`
[+] Added tables
- Blog
`)
[+] Added tables
- Blog
`)
})
it('should diff --from-empty --to-schema-datamodel=./prisma/schema.prisma --script', async () => {
ctx.fixture('schema-only-sqlite')
Expand Down Expand Up @@ -179,9 +195,9 @@ describe('migrate diff', () => {
await expect(result).resolves.toMatchInlineSnapshot(``)
expect(ctx.mocked['console.info'].mock.calls.join('\n')).toMatchInlineSnapshot(`
[-] Removed tables
- Blog
`)
[-] Removed tables
- Blog
`)
})
it('should diff --from-schema-datamodel=./prisma/schema.prisma --to-empty --script', async () => {
ctx.fixture('schema-only-sqlite')
Expand All @@ -208,6 +224,75 @@ describe('migrate diff', () => {
await expect(result).resolves.toMatchInlineSnapshot(``)
expect(ctx.mocked['console.info'].mock.calls.join('\n')).toMatchInlineSnapshot(`No difference detected.`)
})

describe('--exit-code', () => {
it('should exit with code 2 when diff is not empty without --script', async () => {
ctx.fixture('schema-only-sqlite')

const mockExit = jest.spyOn(process, 'exit').mockImplementation()

const result = MigrateDiff.new().parse([
'--preview-feature',
'--from-schema-datamodel=./prisma/schema.prisma',
'--to-empty',
'--exit-code',
])

await expect(result).resolves.toMatchInlineSnapshot(``)
expect(ctx.mocked['console.error'].mock.calls.join('\n')).toMatchInlineSnapshot(``)
expect(ctx.mocked['console.info'].mock.calls.join('\n')).toMatchInlineSnapshot(`
[-] Removed tables
- Blog
`)

expect(mockExit).toHaveBeenCalledTimes(1)
expect(mockExit).toHaveBeenCalledWith(2)
mockExit.mockRestore()
})

it('should exit with code 2 when diff is not empty with --script', async () => {
ctx.fixture('schema-only-sqlite')

const mockExit = jest.spyOn(process, 'exit').mockImplementation()

const result = MigrateDiff.new().parse([
'--preview-feature',
'--from-schema-datamodel=./prisma/schema.prisma',
'--to-empty',
'--script',
'--exit-code',
])

await expect(result).resolves.toMatchInlineSnapshot(``)
expect(ctx.mocked['console.error'].mock.calls.join('\n')).toMatchInlineSnapshot(``)
expect(ctx.mocked['console.info'].mock.calls.join('\n')).toMatchInlineSnapshot(`
-- DropTable
PRAGMA foreign_keys=off;
DROP TABLE "Blog";
PRAGMA foreign_keys=on;
`)

expect(mockExit).toHaveBeenCalledTimes(1)
expect(mockExit).toHaveBeenCalledWith(2)
mockExit.mockRestore()
})

it('should exit with code 0 when diff is empty with --script', async () => {
ctx.fixture('empty')

const result = MigrateDiff.new().parse([
'--preview-feature',
'--from-empty',
'--to-url=file:doesnotexists.db',
'--script',
'--exit-code',
])

await expect(result).resolves.toMatchInlineSnapshot(``)
expect(ctx.mocked['console.info'].mock.calls.join('\n')).toMatchInlineSnapshot(`-- This is an empty migration.`)
})
})
})

describe('mongodb', () => {
Expand Down
60 changes: 38 additions & 22 deletions packages/migrate/src/commands/DbExecute.ts
Expand Up @@ -9,6 +9,28 @@ import { Migrate } from '../Migrate'
import type { EngineArgs } from '../types'
import { DbExecuteNeedsPreviewFeatureFlagError } from '../utils/errors'

const helpOptions = format(
`${chalk.bold('Usage')}
${chalk.dim('$')} prisma db execute --preview-feature [options]
${chalk.bold('Options')}
-h, --help Display this help message
${chalk.italic('Datasource input, only 1 must be provided:')}
--url URL of the datasource to run the command on
--schema Path to your Prisma schema file to take the datasource URL from
${chalk.italic('Script input, only 1 must be provided:')}
--file Path to a file. The content will be sent as the script to be executed
${chalk.bold('Flags')}
--preview-feature Run Preview Prisma commands
--stdin Use the terminal standard input as the script to be executed`,
)

export class DbExecute implements Command {
public static new(): DbExecute {
return new DbExecute()
Expand All @@ -34,34 +56,28 @@ The whole script will be sent as a single command to the database.
${chalk.italic(`This command is currently not supported on MongoDB.`)}
${chalk.bold('Usage')}
${chalk.dim('$')} prisma db execute --preview-feature [options]
${chalk.bold('Options')}
-h, --help Display this help message
${chalk.italic('Datasource input, only 1 must be provided:')}
--url URL of the datasource to run the command on
--schema Path to your Prisma schema file to take the datasource URL from
${chalk.italic('Script input, only 1 must be provided:')}
--stdin Use the terminal standard input as the script to be executed
--file Path to a file. The content will be sent as the script to be executed
${helpOptions}
${chalk.bold('Examples')}
Execute the content of a SQL script file to the datasource URL taken from the schema
${chalk.dim('$')} prisma db execute --preview-feature --file ./script.sql --schema schema.prisma
${chalk.dim('$')} prisma db execute
--preview-feature \\
--file ./script.sql \\
--schema schema.prisma
Execute the SQL script from stdin to the datasource URL specified via the \`DATABASE_URL\` environment variable
${chalk.dim('$')} echo 'TRUNCATE TABLE dev;' | prisma db execute --preview-feature --stdin --url="$DATABASE_URL"
${chalk.dim('$')} echo 'TRUNCATE TABLE dev;' | \\
prisma db execute \\
--preview-feature \\
--stdin \\
--url="$DATABASE_URL"
Like previous example, but exposing the datasource url credentials to your terminal history
${chalk.dim(
'$',
)} echo 'TRUNCATE TABLE dev;' | prisma db execute --preview-feature --stdin --url="mysql://root:root@localhost/mydb"
${chalk.dim('$')} echo 'TRUNCATE TABLE dev;' | \\
prisma db execute \\
--preview-feature \\
--stdin \\
--url="mysql://root:root@localhost/mydb"
`)

public async parse(argv: string[]): Promise<string | Error> {
Expand Down Expand Up @@ -178,7 +194,7 @@ See \`${chalk.green(getCommandWithExecutor('prisma db execute -h'))}\``,

public help(error?: string): string | HelpError {
if (error) {
return new HelpError(`\n${chalk.bold.red(`!`)} ${error}\n${DbExecute.help}`)
throw new HelpError(`\n${error}\n\n${helpOptions}`)
}
return DbExecute.help
}
Expand Down
34 changes: 21 additions & 13 deletions packages/migrate/src/commands/MigrateDiff.ts
Expand Up @@ -32,9 +32,10 @@ ${chalk.italic('Shadow database (only required if using --from-migrations or --t
${chalk.italic('Output format:')}
--script Render a SQL script to stdout instead of the default human readable summary (not supported on MongoDB)
${chalk.bold('Flag')}
${chalk.bold('Flags')}
--preview-feature Run Preview Prisma commands`,
--preview-feature Run Preview Prisma commands
--exit-code Change the exit code behaviour when diff is not empty (Empty: 0, Error: 1, Non empty: 2)`,
)

export class MigrateDiff implements Command {
Expand Down Expand Up @@ -70,44 +71,45 @@ ${chalk.bold('Examples')}
From database to database as summary
e.g. compare two live databases
${chalk.dim('$')} prisma migrate diff \\
--preview-feature \\
${chalk.dim('$')} prisma migrate diff --preview-feature \\
--from-url "$DATABASE_URL" \\
--to-url "postgresql://login:password@localhost:5432/db2"
From a live database to a Prisma datamodel
e.g. roll forward after a migration failed in the middle
${chalk.dim('$')} prisma migrate diff \\
--preview-feature \\
${chalk.dim('$')} prisma migrate diff --preview-feature \\
--shadow-database-url "$SHADOW_DB" \\
--from-url "$PROD_DB" \\
--to-schema-datamodel=next_datamodel.prisma \\
--script
From a live database to a datamodel
e.g. roll backward after a migration failed in the middle
${chalk.dim('$')} prisma migrate diff \\
--preview-feature \\
${chalk.dim('$')} prisma migrate diff --preview-feature \\
--shadow-database-url "$SHADOW_DB" \\
--from-url "$PROD_DB" \\
--to-schema-datamodel=previous_datamodel.prisma \\
--script
From a Prisma Migrate \`migrations\` directory to another database
e.g. generate a migration for a hotfix already applied on production
${chalk.dim('$')} prisma migrate diff \\
--preview-feature \\
${chalk.dim('$')} prisma migrate diff --preview-feature \\
--shadow-database-url "$SHADOW_DB" \\
--from-migrations ./migrations \\
--to-url "$PROD_DB" \\
--script
Execute the --script output with \`prisma db execute\` using bash pipe \`|\`
${chalk.dim('$')} prisma migrate diff \\
--preview-feature \\
${chalk.dim('$')} prisma migrate diff --preview-feature \\
--from-[...] \\
--to-[...] \\
--to-[...] \\
--script | prisma db execute --preview-feature --stdin --url="$DATABASE_URL"
Detect if both sources are in sync, it will exit with exit code 2 if changes are detected
${chalk.dim('$')} prisma migrate diff --preview-feature \\
--exit-code \\
--from-[...] \\
--to-[...]
`)

public async parse(argv: string[]): Promise<string | Error> {
Expand All @@ -131,6 +133,7 @@ ${chalk.bold('Examples')}
// Others
'--shadow-database-url': String,
'--script': Boolean,
'--exit-code': Boolean,
'--preview-feature': Boolean,
'--telemetry-information': String,
},
Expand Down Expand Up @@ -244,6 +247,7 @@ ${chalk.bold('Examples')}
to: to!,
script: args['--script'] || false, // default is false
shadowDatabaseUrl: args['--shadow-database-url'],
exitCode: args['--exit-code'],
})
} finally {
// Stop engine
Expand All @@ -252,6 +256,10 @@ ${chalk.bold('Examples')}

debug(result)

if (args['--exit-code'] && result.exitCode) {
process.exit(result.exitCode)
}

// Return nothing
return ``
}
Expand Down
18 changes: 17 additions & 1 deletion packages/migrate/src/types.ts
Expand Up @@ -179,6 +179,9 @@ export namespace EngineArgs {
// The URL to a live database to use as a shadow database. The schema and data on that database will be wiped during diffing.
// This is only necessary when one of from or to is referencing a migrations directory as a source for the schema.
shadowDatabaseUrl?: string
// Change the exit code behaviour when diff is not empty
// Empty: 0, Error: 1, Non empty: 2
exitCode?: boolean
}

export interface SchemaPush {
Expand Down Expand Up @@ -233,7 +236,20 @@ export namespace EngineResults {
unexecutable: string[]
}
export interface DbExecuteOutput {}
export interface MigrateDiffOutput {}

export enum MigrateDiffExitCode {
// 0 = success
// if --exit-code is passed
// 0 = success with empty diff (no changes)
SUCCESS = 0,
// 1 = Error
ERROR = 1,
// 2 = Succeeded with non-empty diff (changes present)
SUCCESS_NONEMPTY = 2,
}
export interface MigrateDiffOutput {
exitCode: MigrateDiffExitCode
}
}

export interface FileMap {
Expand Down

0 comments on commit f392edc

Please sign in to comment.