Skip to content

Commit

Permalink
Merge 4836b57 into 0b49a8e
Browse files Browse the repository at this point in the history
  • Loading branch information
B4nan committed Aug 14, 2019
2 parents 0b49a8e + 4836b57 commit 17288a3
Show file tree
Hide file tree
Showing 9 changed files with 389 additions and 1 deletion.
4 changes: 4 additions & 0 deletions lib/metadata/MetadataStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export class MetadataStorage {
return this.metadata[entity] = meta;
}

reset(entity: string): void {
delete this.metadata[entity];
}

decorate(em: EntityManager): void {
Object.values(this.metadata)
.filter(meta => meta.prototype && !Utils.isEntity(meta.prototype))
Expand Down
8 changes: 8 additions & 0 deletions lib/schema/MySqlSchemaHelper.ts
Original file line number Diff line number Diff line change
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()`;
}

}
8 changes: 8 additions & 0 deletions lib/schema/PostgreSqlSchemaHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,16 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
return super.getTypeDefinition(prop, PostgreSqlSchemaHelper.TYPES, PostgreSqlSchemaHelper.DEFAULT_TYPE_LENGTHS);
}

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

indexForeignKeys() {
return false;
}

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

}
107 changes: 106 additions & 1 deletion lib/schema/SchemaGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { ColumnBuilder, SchemaBuilder, 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';
import { MetadataStorage } from '../metadata';

export interface TableDefinition {
table_name: string;
}

export class SchemaGenerator {

private readonly platform: Platform = this.driver.getPlatform();
Expand Down Expand Up @@ -55,6 +59,47 @@ export class SchemaGenerator {
return this.wrapSchema(ret + '\n', wrap);
}

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

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

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

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

for (const table of remove) {
ret += this.dump(this.dropTable(table.table_name));
}

return this.wrapSchema(ret, wrap);
}

private async getUpdateTableSQL(meta: EntityMetadata): Promise<string> {
const hasTable = await this.knex.schema.hasTable(meta.collection);
let ret = '';

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

return ret;
}

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

return ret;
}

async execute(sql: string) {
const lines = sql.split('\n').filter(i => i.trim());

Expand Down Expand Up @@ -85,6 +130,58 @@ export class SchemaGenerator {
});
}

private updateTable(meta: EntityMetadata, existingColumns: ColumnInfo): SchemaBuilder[] {
const props = Object.values(meta.properties).filter(prop => this.shouldHaveColumn(prop, true));
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;
}

if (this.helper.supportsColumnAlter() && !this.helper.isSame(prop, column)) {
update.push(prop);
}
}

if (create.length + update.length + remove.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 dropTable(name: string): SchemaBuilder {
let builder = this.knex.schema.dropTableIfExists(name);

Expand Down Expand Up @@ -122,6 +219,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
55 changes: 55 additions & 0 deletions lib/schema/SchemaHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ export abstract class SchemaHelper {
return type;
}

isSame(prop: EntityProperty, info: ColumnInfo, types: Record<string, string[]> = {}, defaultValues: Record<string, string[]> = {}): boolean {
const sameTypes = this.hasSameType(prop, info.type, types);
const sameNullable = info.nullable === !!prop.nullable;
const sameDefault = this.hasSameDefaultValue(info, prop, defaultValues);

return sameTypes && sameNullable && sameDefault;
}

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

supportsColumnAlter(): boolean {
return true;
}

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

private hasSameType(prop: EntityProperty, infoType: string, types: Record<string, string[]>): boolean {
infoType = infoType.replace(/\([?\d]+\)/, '').toLowerCase();
const type = Object.values(types).find(t => t.some(tt => tt.replace(/\([?\d]+\)/, '').toLowerCase() === infoType));

if (!type) {
return false;
}

const columnType = prop.columnType && prop.columnType.replace(/\([?\d]+\)/, '').toLowerCase();
const propTypes = type.map(t => t.replace(/\([?\d]+\)/, '').toLowerCase());

return propTypes.includes(columnType);
}

private hasSameDefaultValue(info: ColumnInfo, prop: EntityProperty, defaultValues: Record<string, string[]>): boolean {
if (info.defaultValue && prop.default) {
const defaultValue = info.defaultValue.toString().replace(/\([?\d]+\)/, '').toLowerCase();
const propDefault = prop.default.toString().toLowerCase();
const same = prop.default.toString() === info.defaultValue.toString().toLowerCase();
const equal = same || propDefault === defaultValue;

return equal || Object.keys(defaultValues).map(t => t.replace(/\([?\d]+\)/, '').toLowerCase()).includes(defaultValue);
}

if (info.defaultValue === null || info.defaultValue.toString().startsWith('nextval(')) {
return prop.default === undefined;
}

if (prop.type === 'boolean') {
return +info.defaultValue === +prop.default; // convert to number first to support `0` as bool
}

if (prop.type === 'boolean' || prop.type === 'number') {
return +info.defaultValue === +prop.default.toString(); // convert to number first to support `0` as bool
}

return !info.defaultValue === !prop.default; // convert to boolean first
}

}
12 changes: 12 additions & 0 deletions lib/schema/SqliteSchemaHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export class SqliteSchemaHelper extends SchemaHelper {
return 'pragma foreign_keys = on;\n';
}

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

getTypeDefinition(prop: EntityProperty): string {
const t = prop.type.toLowerCase() as keyof typeof SqliteSchemaHelper.TYPES;
return (SqliteSchemaHelper.TYPES[t] || SqliteSchemaHelper.TYPES.string)[0];
Expand All @@ -28,4 +32,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`;
}

}

0 comments on commit 17288a3

Please sign in to comment.