Skip to content

Commit

Permalink
feat(schema): add safe and dropTables options to schema generator
Browse files Browse the repository at this point in the history
Closes #416
  • Loading branch information
Martin Adamek committed Mar 26, 2020
1 parent f1afaa6 commit 2d2c73d
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 14 deletions.
2 changes: 2 additions & 0 deletions docs/docs/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ await MikroORM.init({
transactional: true, // wrap each migration in a transaction
disableForeignKeys: true, // wrap statements with `set foreign_key_checks = 0` or equivalent
allOrNothing: true, // wrap all migrations in master transaction
dropTables: true, // allow to disable table dropping
safe: false, // allow to disable table and column dropping
emit: 'ts', // migration generation mode
},
})
Expand Down
17 changes: 17 additions & 0 deletions docs/docs/schema-generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@
title: Schema Generator
---

> SchemaGenerator can do harm to your database. It will drop or alter tables, indexes,
> sequences and such. Please use this tool with caution in development and not on a
> production server. It is meant for helping you develop your Database Schema, but NOT
> with migrating schema from A to B in production. A safe approach would be generating
> the SQL on development server and saving it into SQL Migration files that are executed
> manually on the production server.
> SchemaTool assumes your project uses the given database on its own. Update and Drop
> commands will mess with other tables if they are not related to the current project
> that is using MikroORM. Please be careful!
To generate schema from your entity metadata, you can use `SchemaGenerator` helper.

You can use it via CLI:
Expand All @@ -18,9 +29,15 @@ npx mikro-orm schema:drop --dump # Dumps drop schema SQL
`schema:create` will automatically create the database if it does not exist.

`schema:update` drops all unknown tables by default, you can use `--no-drop-tables`
to get around it. There is also `--safe` flag that will disable both table dropping as
well as column dropping.

`schema:drop` will by default drop all database tables. You can use `--drop-db` flag to drop
the whole database instead.

## Using SchemaGenerator programmatically

Or you can create simple script where you initialize MikroORM like this:

**`./create-schema.ts`**
Expand Down
38 changes: 35 additions & 3 deletions lib/cli/SchemaCommandFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ export class SchemaCommandFactory {
desc: 'Do not skip foreign key checks',
});

if (command === 'update') {
args.option('safe', {
type: 'boolean',
desc: 'Allows to disable table and column dropping',
default: false,
});
args.option('drop-tables', {
type: 'boolean',
desc: 'Allows to disable table dropping',
default: true,
});
}

if (command === 'drop') {
args.option('drop-migrations-table', {
type: 'boolean',
Expand All @@ -65,20 +78,39 @@ export class SchemaCommandFactory {

const orm = await CLIHelper.getORM();
const generator = orm.getSchemaGenerator();
const params = SchemaCommandFactory.getOrderedParams(args, method);

if (args.dump) {
const m = `get${method.substr(0, 1).toUpperCase()}${method.substr(1)}SchemaSQL`;
const dump = await generator[m](!args.fkChecks, args.dropMigrationsTable);
const dump = await generator[m](...params);
CLIHelper.dump(dump, orm.config, 'sql');
} else {
const m = method + 'Schema';
await generator[m](!args.fkChecks, args.dropMigrationsTable, args.dropDb);
await generator[m](...params);
CLIHelper.dump(chalk.green(successMessage));
}

await orm.close(true);
}

private static getOrderedParams(args: Arguments<Options>, method: 'create' | 'update' | 'drop'): any[] {
const ret: any[] = [!args.fkChecks];

if (method === 'update') {
ret.push(args.safe, args.dropTables);
}

if (method === 'drop') {
ret.push(args.dropMigrationsTable);

if (!args.dump) {
ret.push(args.dropDb);
}
}

return ret;
}

}

export type Options = { dump: boolean; run: boolean; fkChecks: boolean; dropMigrationsTable: boolean; dropDb: boolean };
export type Options = { dump: boolean; run: boolean; fkChecks: boolean; dropMigrationsTable: boolean; dropDb: boolean; dropTables: boolean; safe: boolean };
2 changes: 1 addition & 1 deletion lib/migrations/Migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class Migrator {
}

private async getSchemaDiff(): Promise<string[]> {
const dump = await this.schemaGenerator.getUpdateSchemaSQL(false);
const dump = await this.schemaGenerator.getUpdateSchemaSQL(false, this.options.safe, this.options.dropTables);
const lines = dump.split('\n');

for (let i = lines.length - 1; i > 0; i--) {
Expand Down
26 changes: 17 additions & 9 deletions lib/schema/SchemaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,23 +81,27 @@ export class SchemaGenerator {
return this.wrapSchema(ret + '\n', wrap);
}

async updateSchema(wrap = true): Promise<void> {
const sql = await this.getUpdateSchemaSQL(wrap);
async updateSchema(wrap: boolean = true, safe = false, dropTables = true): Promise<void> {
const sql = await this.getUpdateSchemaSQL(wrap, safe, dropTables);
await this.execute(sql);
}

async getUpdateSchemaSQL(wrap = true): Promise<string> {
async getUpdateSchemaSQL(wrap = true, safe = false, dropTables = true): Promise<string> {
const schema = await DatabaseSchema.create(this.connection, this.helper, this.config);
let ret = '';

for (const meta of Object.values(this.metadata.getAll())) {
ret += this.getUpdateTableSQL(meta, schema);
ret += this.getUpdateTableSQL(meta, schema, safe);
}

for (const meta of Object.values(this.metadata.getAll())) {
ret += this.getUpdateTableFKsSQL(meta, schema);
}

if (!dropTables || safe) {
return this.wrapSchema(ret, wrap);
}

const definedTables = Object.values(this.metadata.getAll()).map(meta => meta.collection);
const remove = schema.getTables().filter(table => !definedTables.includes(table.name));

Expand Down Expand Up @@ -131,14 +135,14 @@ export class SchemaGenerator {
}
}

private getUpdateTableSQL(meta: EntityMetadata, schema: DatabaseSchema): string {
private getUpdateTableSQL(meta: EntityMetadata, schema: DatabaseSchema, safe: boolean): string {
const table = schema.getTable(meta.collection);

if (!table) {
return this.dump(this.createTable(meta));
}

return this.updateTable(meta, table).map(builder => this.dump(builder)).join('\n');
return this.updateTable(meta, table, safe).map(builder => this.dump(builder)).join('\n');
}

private getUpdateTableFKsSQL(meta: EntityMetadata, schema: DatabaseSchema): string {
Expand Down Expand Up @@ -186,8 +190,8 @@ export class SchemaGenerator {
});
}

private updateTable(meta: EntityMetadata, table: DatabaseTable): SchemaBuilder[] {
const { create, update, remove } = this.computeTableDifference(meta, table);
private updateTable(meta: EntityMetadata, table: DatabaseTable, safe: boolean): SchemaBuilder[] {
const { create, update, remove } = this.computeTableDifference(meta, table, safe);

if (create.length + update.length + remove.length === 0) {
return [];
Expand Down Expand Up @@ -217,7 +221,7 @@ export class SchemaGenerator {
return ret;
}

private computeTableDifference(meta: EntityMetadata, table: DatabaseTable): { create: EntityProperty[]; update: { prop: EntityProperty; column: Column; diff: IsSame }[]; remove: Column[] } {
private computeTableDifference(meta: EntityMetadata, table: DatabaseTable, safe: boolean): { create: EntityProperty[]; update: { prop: EntityProperty; column: Column; diff: IsSame }[]; remove: Column[] } {
const props = Object.values(meta.properties).filter(prop => this.shouldHaveColumn(meta, prop, true));
const columns = table.getColumns();
const create: EntityProperty[] = [];
Expand All @@ -228,6 +232,10 @@ export class SchemaGenerator {
this.computeColumnDifference(table, prop, create, update);
}

if (safe) {
return { create, update, remove: [] };
}

return { create, update, remove };
}

Expand Down
4 changes: 4 additions & 0 deletions lib/utils/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export class Configuration<D extends IDatabaseDriver = IDatabaseDriver> {
transactional: true,
disableForeignKeys: true,
allOrNothing: true,
dropTables: true,
safe: false,
emit: 'ts',
},
cache: {
Expand Down Expand Up @@ -262,6 +264,8 @@ export type MigrationsOptions = {
transactional?: boolean;
disableForeignKeys?: boolean;
allOrNothing?: boolean;
dropTables?: boolean;
safe?: boolean;
emit?: 'js' | 'ts';
};

Expand Down
2 changes: 2 additions & 0 deletions tests/SchemaGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ describe('SchemaGenerator', () => {

meta.reset('Author2');
meta.reset('NewTable');
await expect(generator.getUpdateSchemaSQL(false, true)).resolves.toMatchSnapshot('mysql-update-schema-drop-table-safe');
await expect(generator.getUpdateSchemaSQL(false, false, false)).resolves.toMatchSnapshot('mysql-update-schema-drop-table-disabled');
await expect(generator.getUpdateSchemaSQL(false)).resolves.toMatchSnapshot('mysql-update-schema-drop-table');
await generator.updateSchema();

Expand Down
4 changes: 4 additions & 0 deletions tests/__snapshots__/SchemaGenerator.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,10 @@ drop table if exists \`new_table\`;
"
`;

exports[`SchemaGenerator update schema [mysql]: mysql-update-schema-drop-table-disabled 1`] = `""`;

exports[`SchemaGenerator update schema [mysql]: mysql-update-schema-drop-table-safe 1`] = `""`;

exports[`SchemaGenerator update schema [mysql]: mysql-update-schema-rename-column 1`] = `
"alter table \`author2\` change \`age\` \`age_in_years\` int(11) null default 42;
Expand Down
18 changes: 17 additions & 1 deletion tests/cli/CLIHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ describe('CLIHelper', () => {
pathExistsMock.mockRestore();
});

test('builder', async () => {
test('builder (schema drop)', async () => {
const args = { option: jest.fn() };
SchemaCommandFactory.configureSchemaCommand(args as any, 'drop');
expect(args.option.mock.calls.length).toBe(5);
Expand All @@ -129,6 +129,22 @@ describe('CLIHelper', () => {
expect(args.option.mock.calls[4][1]).toMatchObject({ type: 'boolean' });
});

test('builder (schema update)', async () => {
const args = { option: jest.fn() };
SchemaCommandFactory.configureSchemaCommand(args as any, 'update');
expect(args.option.mock.calls.length).toBe(5);
expect(args.option.mock.calls[0][0]).toBe('r');
expect(args.option.mock.calls[0][1]).toMatchObject({ alias: 'run', type: 'boolean' });
expect(args.option.mock.calls[1][0]).toBe('d');
expect(args.option.mock.calls[1][1]).toMatchObject({ alias: 'dump', type: 'boolean' });
expect(args.option.mock.calls[2][0]).toBe('fk-checks');
expect(args.option.mock.calls[2][1]).toMatchObject({ type: 'boolean' });
expect(args.option.mock.calls[3][0]).toBe('safe');
expect(args.option.mock.calls[3][1]).toMatchObject({ type: 'boolean' });
expect(args.option.mock.calls[4][0]).toBe('drop-tables');
expect(args.option.mock.calls[4][1]).toMatchObject({ type: 'boolean' });
});

test('dump', async () => {
log.mock.calls.length = 0;
CLIHelper.dump('test');
Expand Down

0 comments on commit 2d2c73d

Please sign in to comment.