Skip to content

Commit

Permalink
feat(migrate): add schema push command (#3174)
Browse files Browse the repository at this point in the history
Co-authored-by: prisma-bot <prismabots@gmail.com>
  • Loading branch information
Jolg42 and prisma-bot committed Jul 30, 2020
1 parent b1d5a6d commit 53b27d6
Show file tree
Hide file tree
Showing 11 changed files with 484 additions and 3 deletions.
24 changes: 24 additions & 0 deletions src/packages/migrate/src/Migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ const del = promisify(rimraf)
const readFile = promisify(fs.readFile)
const exists = promisify(fs.exists)

export interface PushOptions {
force?: boolean
}
export interface UpOptions {
preview?: boolean
n?: number
Expand Down Expand Up @@ -320,6 +323,27 @@ export class Migrate {
return initLockFile()
}

public async push({ force = false }: PushOptions = {}): Promise<
EngineResults.SchemaPush
> {
const datamodel = this.getDatamodel()

const {
warnings,
unexecutable,
executedSteps,
} = await this.engine.schemaPush({
force,
schema: datamodel,
})

return {
executedSteps,
warnings,
unexecutable,
}
}

public async createMigration(
migrationId: string,
): Promise<LocalMigrationWithDatabaseSteps | undefined> {
Expand Down
5 changes: 5 additions & 0 deletions src/packages/migrate/src/MigrateEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ export class MigrateEngine {
public stop(): void {
this.child!.kill()
}
public schemaPush(
args: EngineArgs.SchemaPush,
): Promise<EngineResults.SchemaPush> {
return this.runCommand(this.getRPCPayload('schemaPush', args))
}
public applyMigration(
args: EngineArgs.ApplyMigration,
): Promise<EngineResults.ApplyMigration> {
Expand Down
202 changes: 202 additions & 0 deletions src/packages/migrate/src/__tests__/Schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import assert from 'assert'
import del from 'del'
import mkdir from 'make-dir'
import fs from 'fs'
import { promisify } from 'util'
import { dirname, join } from 'path'
import pkgup from 'pkg-up'
import dedent from 'strip-indent'
import Sqlite from 'better-sqlite3'
import stripAnsi from 'strip-ansi'
import { Migrate } from '../Migrate'
import { SchemaPush } from '../commands/SchemaPush'

const writeFile = promisify(fs.writeFile)

describe('schema.create', () => {
createTests().map((t) => {
// eslint-disable-next-line jest/expect-expect
test(t.name, async () => {
const pkg = dirname((await pkgup({ cwd: __dirname })) || __filename)
const root = join(pkg, 'tmp', 'schema-' + Date.now())
const schemaPath = join(root, Object.keys(t.fs)[0])
await writeFiles(root, t.fs)
await t.fn(schemaPath)
await del(root)
})
})
})

// create a temporary set of files
async function writeFiles(
root: string,
files: {
[name: string]: any // eslint-disable-line @typescript-eslint/no-explicit-any
},
): Promise<string> {
for (const name in files) {
const filepath = join(root, name)
await mkdir(dirname(filepath))
await writeFile(filepath, dedent(files[name]))
}
// return the test path
return root
}

// create file tests
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function createTests() {
return [
{
name: 'simple ok',
fs: {
'schema.prisma': `
datasource my_db {
provider = "sqlite"
url = "file:./db/db_file.db"
default = true
}
model User {
id Int @id
}
`,
'db/.keep': ``,
},
fn: async (schemaPath: string): Promise<undefined> => {
const migrate = new Migrate(schemaPath)
const migration = await migrate.push()
migrate.stop()
expect(migration.executedSteps).toEqual(4)
expect(migration.warnings).toEqual([])
expect(migration.unexecutable).toEqual([])
return
},
},
{
name: 'custom schema filename ok',
fs: {
'myawesomeschema.file': `
datasource my_db {
provider = "sqlite"
url = "file:./db/db_file.db"
default = true
}
model User {
id Int @id
}
`,
'db/.keep': ``,
},
fn: async (schemaPath: string): Promise<undefined> => {
const migrate = new Migrate(schemaPath)
const migration = await migrate.push()
migrate.stop()
expect(migration.executedSteps).toEqual(4)
expect(migration.warnings).toEqual([])
expect(migration.unexecutable).toEqual([])
return
},
},
{
name: 'custom folder and schema filename name ok',
fs: {
'awesome/myawesomeschema.file': `
datasource my_db {
provider = "sqlite"
url = "file:../db/db_file.db"
default = true
}
model User {
id Int @id
}
`,
'db/.keep': ``,
},
fn: async (schemaPath: string): Promise<undefined> => {
const migrate = new Migrate(schemaPath)
const migration = await migrate.push()
migrate.stop()
expect(migration.executedSteps).toEqual(4)
expect(migration.warnings).toEqual([])
expect(migration.unexecutable).toEqual([])
return
},
},
{
name: 'invalid ok',
fs: {
'schema.prisma': `
datasource my_db {
provider = "sqlite"
url = "file:./db/db_file.db"
default = true
}
model User {
id Int @id
canBeNull String?
}
`,
'schema-not-null.prisma': `
datasource my_db {
provider = "sqlite"
url = "file:./db/db_file.db"
default = true
}
model User {
id Int @id
canBeNull String
requiredSomething String
}
`,
'db/.keep': ``,
},
fn: async (schemaPath: string): Promise<undefined> => {
const migrate = new Migrate(schemaPath)
const migration = await migrate.push()
migrate.stop()
expect(migration.executedSteps).toEqual(4)
expect(migration.warnings).toEqual([])
expect(migration.unexecutable).toEqual([])

const db = new Sqlite(
schemaPath.replace('schema.prisma', 'db/db_file.db'),
{
// verbose: console.log,
},
)
const stmt = db.prepare('INSERT INTO User (canBeNull) VALUES (?)')
const info = stmt.run('Something!')
assert.equal(info.changes, 1)

const oldConsoleLog = console.log
const logs: string[] = []
console.log = (...args) => {
logs.push(...args)
}

const schemaPath2 = schemaPath.replace(
'schema.prisma',
'schema-not-null.prisma',
)

try {
await SchemaPush.new().parse([
`--schema=${schemaPath2}`,
'--experimental',
])
} catch (e) {
// Should error with unexecutableMigrations:
expect(stripAnsi(e.message)).toMatchSnapshot()
}
console.log = oldConsoleLog
expect(stripAnsi(logs.join('\n'))).toMatchSnapshot()
return
},
},
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,8 @@ exports[`migrate.create invalid ok 6`] = `
⚠️ We found migration(s) that cannot be executed:
• Added the required column \`requiredSomething\` to the \`User\` table without a default value. There are 1 rows in this table, it is not possible to execute this migration."
• Added the required column \`requiredSomething\` to the \`User\` table without a default value. There are 1 rows in this table, it is not possible to execute this migration.
"
`;

exports[`migrate.create invalid ok 7`] = `
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`schema.create invalid ok 1`] = `
"
⚠️ We found changes that cannot be executed:
• Added the required column \`requiredSomething\` to the \`User\` table without a default value. There are 1 rows in this table, it is not possible to execute this migration.
"
`;

exports[`schema.create invalid ok 2`] = `""`;
5 changes: 5 additions & 0 deletions src/packages/migrate/src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { MigrateDown } from './commands/MigrateDown'
import { MigrateSave } from './commands/MigrateSave'
import { MigrateTmpPrepare } from './commands/MigrateTmpPrepare'
import { MigrateUp } from './commands/MigrateUp'
import { SchemaCommand } from './commands/SchemaCommand'
import { SchemaPush } from './commands/SchemaPush'
import { StudioCommand } from './commands/StudioCommand'
import { handlePanic } from './utils/handlePanic'
import { ProviderAliases } from '@prisma/sdk'
Expand Down Expand Up @@ -51,6 +53,9 @@ async function main(): Promise<number> {
down: MigrateDown.new(),
['tmp-prepare']: MigrateTmpPrepare.new(),
studio: StudioCommand.new(providerAliases),
schema: SchemaCommand.new({
push: SchemaPush.new(),
}),
})
// parse the arguments
const result = await cli.parse(process.argv.slice(2))
Expand Down
2 changes: 1 addition & 1 deletion src/packages/migrate/src/commands/MigrateSave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export class MigrateSave implements Command {
messages.push(`${chalk(` • ${item.description}`)}`)
}
console.log() // empty line
throw new Error(messages.join('\n'))
throw new Error(`${messages.join('\n')}\n`)
}

if (migration.warnings && migration.warnings.length > 0) {
Expand Down
92 changes: 92 additions & 0 deletions src/packages/migrate/src/commands/SchemaCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {
arg,
Command,
Commands,
format,
HelpError,
isError,
unknownCommand,
} from '@prisma/sdk'
import chalk from 'chalk'

export class SchemaCommand implements Command {
public static new(cmds: Commands): SchemaCommand {
return new SchemaCommand(cmds)
}

// static help template
private static help = format(`
${
process.platform === 'win32' ? '' : chalk.bold('🏋️ ')
}Powerful Prisma schema commands from your terminal
${chalk.bold.yellow('WARNING')} ${chalk.bold(
'This functionality is currently in an experimental state.',
)}
${chalk.dim(
'When using any of the commands below you need to explicitly opt-in via the --experimental flag.',
)}
${chalk.bold('Usage')}
With an existing schema.prisma:
${chalk.dim('$')} prisma schema [command] [options] --experimental
Or specify a schema:
${chalk.dim(
'$',
)} prisma schema [command] [options] --experimental --schema=./schema.prisma
${chalk.bold('Options')}
-h, --help Display this help message
${chalk.bold('Commands')}
push Push the state from your schema.prisma to your database
${chalk.bold('Examples')}
Using prisma schema push
${chalk.dim('$')} prisma schema push --experimental
`)
private constructor(private readonly cmds: Commands) {}

public async parse(argv: string[]): Promise<string | Error> {
// parse the arguments according to the spec
const args = arg(argv, {
'--help': Boolean,
'-h': '--help',
'--experimental': Boolean,
})

if (isError(args)) {
return this.help(args.message)
}

// display help for help flag or no subcommand
if (args._.length === 0 || args['--help']) {
return this.help()
}

// check if we have that subcommand
const cmd = this.cmds[args._[0]]
if (cmd) {
const argsForCmd = args['--experimental']
? [...args._.slice(1), `--experimental=${args['--experimental']}`]
: args._.slice(1)
return cmd.parse(argsForCmd)
}

return unknownCommand(SchemaCommand.help, args._[0])
}

public help(error?: string): string | HelpError {
if (error) {
return new HelpError(
`\n${chalk.bold.red(`!`)} ${error}\n${SchemaCommand.help}`,
)
}
return SchemaCommand.help
}
}

0 comments on commit 53b27d6

Please sign in to comment.