Skip to content

Commit

Permalink
Merge 5833102 into b56179c
Browse files Browse the repository at this point in the history
  • Loading branch information
B4nan committed Aug 5, 2019
2 parents b56179c + 5833102 commit bba1c47
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 24 deletions.
7 changes: 6 additions & 1 deletion lib/MikroORM.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { EntityManager } from './EntityManager';
import { IDatabaseDriver } from './drivers';
import { AbstractSqlDriver, IDatabaseDriver } from './drivers';
import { MetadataDiscovery } from './metadata';
import { Configuration, Logger, Options } from './utils';
import { EntityMetadata } from './decorators';
import { SchemaGenerator } from './schema';

export class MikroORM {

Expand Down Expand Up @@ -61,4 +62,8 @@ export class MikroORM {
return this.metadata;
}

getSchemaGenerator(): SchemaGenerator {
return new SchemaGenerator(this.driver as AbstractSqlDriver, this.metadata);
}

}
10 changes: 9 additions & 1 deletion lib/schema/MySqlSchemaHelper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SchemaHelper } from './SchemaHelper';
import { EntityProperty } from '../decorators';
import { MySqlTableBuilder } from 'knex';
import { ColumnInfo, MySqlTableBuilder } from 'knex';

export class MySqlSchemaHelper extends SchemaHelper {

Expand Down Expand Up @@ -38,4 +38,12 @@ export class MySqlSchemaHelper extends SchemaHelper {
return super.getTypeDefinition(prop, MySqlSchemaHelper.TYPES, MySqlSchemaHelper.DEFAULT_TYPE_LENGTHS);
}

isSame(prop: EntityProperty, type: ColumnInfo): boolean {
return super.isSame(prop, type, MySqlSchemaHelper.TYPES);
}

getListTablesSQL(): string {
return `select table_name from information_schema.tables where table_type = 'BASE TABLE' and table_schema = schema()`;
}

}
4 changes: 4 additions & 0 deletions lib/schema/PostgreSqlSchemaHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,8 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
return false;
}

getListTablesSQL(): string {
return `select * from information_schema.tables where table_type = 'BASE TABLE' and table_schema not in ('pg_catalog', 'information_schema')`;
}

}
158 changes: 145 additions & 13 deletions lib/schema/SchemaGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,155 @@
import { ColumnBuilder, TableBuilder } from 'knex';
import { ColumnBuilder, ColumnInfo, SchemaBuilder, TableBuilder } from 'knex';
import { AbstractSqlDriver, Cascade, ReferenceType, Utils } from '..';
import { EntityMetadata, EntityProperty } from '../decorators';
import { Platform } from '../platforms';

interface TableDefinition {
table_name: string;
}

export class SchemaGenerator {

private readonly platform: Platform = this.driver.getPlatform();
private readonly helper = this.platform.getSchemaHelper();
private readonly knex = this.driver.getConnection().getKnex();
private readonly connection = this.driver.getConnection();
private readonly knex = this.connection.getKnex();

constructor(private readonly driver: AbstractSqlDriver,
private readonly metadata: Record<string, EntityMetadata>) { }

generate(): string {
let ret = this.helper.getSchemaBeginning();
async generate(): Promise<string> {
let ret = await this.dropSchema(false, false);
ret += await this.createSchema(false, false);

Object.values(this.metadata).forEach(meta => ret += this.knex.schema.dropTableIfExists(meta.collection).toQuery() + ';\n');
ret += '\n';
Object.values(this.metadata).forEach(meta => ret += this.createTable(meta));
Object.values(this.metadata).forEach(meta => {
const alter = this.knex.schema.alterTable(meta.collection, table => this.createForeignKeys(table, meta)).toQuery();
ret += alter ? alter + ';\n\n' : '';
});
return this.wrapSchema(ret);
}

async createSchema(run = false, wrap = true): Promise<string> {
let ret = '';

for (const meta of Object.values(this.metadata)) {
ret += await this.dump(this.createTable(meta), run);
}

for (const meta of Object.values(this.metadata)) {
ret += await this.dump(this.knex.schema.alterTable(meta.collection, table => this.createForeignKeys(table, meta)), run);
}

return this.wrapSchema(ret, wrap);
}

async dropSchema(run = false, wrap = true): Promise<string> {
let ret = '';

for (const meta of Object.values(this.metadata)) {
ret += await this.dump(this.knex.schema.dropTableIfExists(meta.collection), run, '\n');
}

return this.wrapSchema(ret + '\n', wrap);
}

async updateSchema(run = false, wrap = true): Promise<string> {
let ret = '';
const tables = await this.connection.execute<TableDefinition[]>(this.helper.getListTablesSQL());

for (const meta of Object.values(this.metadata)) {
const hasTable = await this.knex.schema.hasTable(meta.collection);

if (!hasTable) {
ret += await this.dump(this.createTable(meta), run);
ret += await this.dump(this.knex.schema.alterTable(meta.collection, table => this.createForeignKeys(table, meta)), run);

continue;
}

const cols = await this.knex(meta.collection).columnInfo();
const sql = await Utils.runSerial(this.updateTable(meta, cols), builder => this.dump(builder, run));
ret += sql.join('\n');
}

const definedTables = Object.values(this.metadata).map(meta => meta.collection);
const remove = tables.filter(table => !definedTables.includes(table.table_name));

for (const table of remove) {
ret += await this.dump(this.knex.schema.dropTable(table.table_name), run);
}

return this.wrapSchema(ret, wrap);
}

private async wrapSchema(sql: string, wrap = true): Promise<string> {
if (!wrap) {
return sql;
}

let ret = this.helper.getSchemaBeginning();
ret += sql;
ret += this.helper.getSchemaEnd();

return ret;
}

private createTable(meta: EntityMetadata): string {
private createTable(meta: EntityMetadata): SchemaBuilder {
return this.knex.schema.createTable(meta.collection, table => {
Object
.values(meta.properties)
.filter(prop => this.shouldHaveColumn(prop))
.forEach(prop => this.createTableColumn(table, prop));
this.helper.finalizeTable(table);
}).toQuery() + ';\n\n';
});
}

private updateTable(meta: EntityMetadata, existingColumns: ColumnInfo): SchemaBuilder[] {
const props = Object.values(meta.properties).filter(prop => this.shouldHaveColumn(prop));
const create: EntityProperty[] = [];
const update: EntityProperty[] = [];
const columns = Object.keys(existingColumns);
const remove = columns.filter(name => !props.find(prop => prop.fieldName === name));
const ret: SchemaBuilder[] = [];

for (const prop of props) {
const col = columns.find(name => name === prop.fieldName);
const column = existingColumns[col as keyof typeof existingColumns] as object as ColumnInfo;

if (!col) {
create.push(prop);
continue;
}

// TODO check whether we need to update the column based on `cols`
if (this.helper.supportsColumnAlter() && !this.helper.isSame(prop, column)) {
update.push(prop);
}
}

if (create.length + update.length === 0) {
return ret;
}

ret.push(this.knex.schema.alterTable(meta.collection, table => {
if (this.helper.supportsColumnAlter()) {
table.dropPrimary();
}

const fks = update.filter(prop => prop.reference !== ReferenceType.SCALAR);
fks.forEach(fk => table.dropForeign([fk.name]));
}));

ret.push(this.knex.schema.alterTable(meta.collection, table => {
for (const prop of create) {
this.createTableColumn(table, prop);
}

for (const prop of update) {
this.updateTableColumn(table, prop);
}

if (remove.length > 0) {
table.dropColumns(...remove);
}
}));

return ret;
}

private shouldHaveColumn(prop: EntityProperty): boolean {
Expand Down Expand Up @@ -66,6 +180,14 @@ export class SchemaGenerator {
return col;
}

private updateTableColumn(table: TableBuilder, prop: EntityProperty, alter = false): ColumnBuilder {
if (prop.primary) {
table.dropPrimary();
}

return this.createTableColumn(table, prop, alter).alter();
}

private configureColumn(prop: EntityProperty, col: ColumnBuilder, alter: boolean) {
const nullable = (alter && this.platform.requiresNullableForAlteringColumn()) || prop.nullable!;
const indexed = prop.reference !== ReferenceType.SCALAR && this.helper.indexForeignKeys();
Expand Down Expand Up @@ -129,4 +251,14 @@ export class SchemaGenerator {
return this.helper.getTypeDefinition(meta.properties[meta.primaryKey]);
}

private async dump(builder: SchemaBuilder, run: boolean, append = '\n\n'): Promise<string> {
if (run) {
await builder;
}

const sql = builder.toQuery();

return sql.length > 0 ? `${sql};${append}` : '';
}

}
15 changes: 14 additions & 1 deletion lib/schema/SchemaHelper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TableBuilder } from 'knex';
import { ColumnInfo, TableBuilder } from 'knex';
import { EntityProperty } from '../decorators';

export abstract class SchemaHelper {
Expand Down Expand Up @@ -27,6 +27,11 @@ export abstract class SchemaHelper {
return type;
}

isSame(prop: EntityProperty, info: ColumnInfo, types: Record<string, string> = {}): boolean {
const t = Object.values(types).find(t => t.replace(/\(.\)$/, '') === info.type);
return t === prop.type && info.nullable === !!prop.nullable && info.defaultValue === prop.default;
}

supportsSchemaConstraints(): boolean {
return true;
}
Expand All @@ -35,4 +40,12 @@ export abstract class SchemaHelper {
return true;
}

supportsColumnAlter(): boolean {
return true;
}

getListTablesSQL(): string {
throw new Error('Not supported by given driver');
}

}
8 changes: 8 additions & 0 deletions lib/schema/SqliteSchemaHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,12 @@ export class SqliteSchemaHelper extends SchemaHelper {
return false;
}

supportsColumnAlter(): boolean {
return false;
}

getListTablesSQL(): string {
return `select name as table_name from sqlite_master where type = 'table' and name != 'sqlite_sequence' and name != 'geometry_columns' and name != 'spatial_ref_sys' union all select name from sqlite_temp_master where type = 'table' order by name`;
}

}
3 changes: 2 additions & 1 deletion tests/MikroORM.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MikroORM, EntityManager, Configuration } from '../lib';
import { MikroORM, EntityManager, Configuration, SchemaGenerator } from '../lib';
import { Author } from './entities';
import { BASE_DIR } from './bootstrap';
import { FooBaz2 } from './entities-sql';
Expand Down Expand Up @@ -36,6 +36,7 @@ describe('MikroORM', () => {

expect(orm).toBeInstanceOf(MikroORM);
expect(orm.em).toBeInstanceOf(EntityManager);
expect(Object.keys(orm.getMetadata())).toEqual(['Author', 'Book', 'FooBar', 'FooBaz', 'Publisher', 'BookTag', 'Test']);
expect(await orm.isConnected()).toBe(true);

await orm.close();
Expand Down
25 changes: 18 additions & 7 deletions tests/SchemaGenerator.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { initORMMySql, initORMPostgreSql, initORMSqlite } from './bootstrap';
import { SchemaGenerator } from '../lib/schema';

/**
* @class SchemaGeneratorTest
Expand All @@ -8,25 +7,37 @@ describe('SchemaGenerator', () => {

test('generate schema from metadata [mysql]', async () => {
const orm = await initORMMySql();
const generator = new SchemaGenerator(orm.em.getDriver(), orm.getMetadata());
const dump = generator.generate();
const generator = orm.getSchemaGenerator();
const dump = await generator.generate();
expect(dump).toMatchSnapshot('mysql-schema-dump');

const ret = await generator.updateSchema();
console.log(ret);

await orm.close(true);
});

test('generate schema from metadata [sqlite]', async () => {
const orm = await initORMSqlite();
const generator = new SchemaGenerator(orm.em.getDriver(), orm.getMetadata());
const dump = generator.generate();
const generator = orm.getSchemaGenerator();
const dump = await generator.generate();
expect(dump).toMatchSnapshot('sqlite-schema-dump');

const ret = await generator.updateSchema();
console.log(ret);

await orm.close(true);
});

test('generate schema from metadata [postgres]', async () => {
const orm = await initORMPostgreSql();
const generator = new SchemaGenerator(orm.em.getDriver(), orm.getMetadata());
const dump = generator.generate();
const generator = orm.getSchemaGenerator();
const dump = await generator.generate();
expect(dump).toMatchSnapshot('postgres-schema-dump');

const ret = await generator.updateSchema();
console.log(ret);

await orm.close(true);
});

Expand Down
1 change: 1 addition & 0 deletions tests/SchemaHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('SchemaHelper', () => {
expect(helper.getSchemaBeginning()).toBe('');
expect(helper.getSchemaEnd()).toBe('');
expect(helper.getTypeDefinition({ type: 'test' } as any)).toBe('test');
expect(() => helper.getListTablesSQL()).toThrow('Not supported by given driver');
});

});

0 comments on commit bba1c47

Please sign in to comment.