added basic sqlite driver

This commit is contained in:
Umed Khudoiberdiev 2016-09-02 23:31:19 +05:00
parent 7fc08acecb
commit 333c212791
52 changed files with 1374 additions and 231 deletions

2
.gitignore vendored
View File

@ -1,6 +1,6 @@
build/
coverage/
node_modules/
typings/
coverage/
npm-debug.log
config/parameters.json

View File

@ -1,42 +1,48 @@
{
"connections": {
"mysql": {
"host": "192.168.99.100",
"host": "localhost",
"port": 3306,
"username": "root",
"password": "admin",
"database": "test"
},
"mysqlSecondary": {
"host": "192.168.99.100",
"host": "localhost",
"port": 3306,
"username": "root",
"password": "admin",
"database": "test2"
},
"mariadb": {
"host": "192.168.99.100",
"host": "localhost",
"port": 3307,
"username": "root",
"password": "admin",
"database": "test"
},
"mariadbSecondary": {
"host": "192.168.99.100",
"host": "localhost",
"port": 3307,
"username": "root",
"password": "admin",
"database": "test2"
},
"sqlite": {
"storage": "temp/sqlitedb.db"
},
"sqliteSecondary": {
"storage": "temp/sqlitedb-secondary.db"
},
"postgres": {
"host": "192.168.99.100",
"host": "localhost",
"port": 5432,
"username": "root",
"password": "admin",
"database": "test"
},
"postgresSecondary": {
"host": "192.168.99.100",
"host": "localhost",
"port": 5432,
"username": "root",
"password": "admin",

View File

@ -14,6 +14,12 @@
"password": "admin",
"database": "test2"
},
"sqlite": {
"storage": "temp/sqlitedb.db"
},
"sqliteSecondary": {
"storage": "temp/sqlitedb-secondary.db"
},
"postgres": {
"host": "localhost",
"port": 5432,

View File

@ -43,11 +43,13 @@
"gulpclass": "0.1.1",
"mariasql": "^0.2.6",
"mocha": "^3.0.1",
"mssql": "^3.3.0",
"mysql": "^2.11.1",
"pg": "^6.0.3",
"remap-istanbul": "^0.6.4",
"sinon": "^1.17.5",
"sinon-chai": "^2.8.0",
"sqlite3": "^3.1.4",
"ts-node": "^1.2.2",
"tslint": "next",
"tslint-stylish": "^2.1.0-beta",

View File

@ -5,7 +5,7 @@ import {Post} from "./entity/Post";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",
@ -17,7 +17,7 @@ const options: ConnectionOptions = {
/*const options: CreateConnectionOptions = {
driver: "postgres",
driverOptions: {
host: "192.168.99.100",
host: "localhost",
port: 5432,
username: "test",
password: "admin",

View File

@ -9,7 +9,7 @@ import {Category} from "./entity/Category";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -5,7 +5,7 @@ import {EverythingEntity} from "./entity/EverythingEntity";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -6,7 +6,7 @@ import {CustomNamingStrategy} from "./naming-strategy/CustomNamingStrategy";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -8,7 +8,7 @@ import {Blog} from "./entity/Blog";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -6,7 +6,7 @@ import {PostAuthor} from "./entity/PostAuthor";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -5,7 +5,7 @@ import {Post} from "./entity/Post";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -5,7 +5,7 @@ import {Post} from "./entity/Post";
const options: ConnectionOptions = {
driver: {
type: "postgres",
host: "192.168.99.100",
host: "localhost",
port: 5432,
username: "root",
password: "admin",

View File

@ -5,7 +5,7 @@ import {Post} from "./entity/Post";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -7,7 +7,7 @@ import {Category} from "./entity/Category";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -8,7 +8,7 @@ import {PostMetadata} from "./entity/PostMetadata";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -11,7 +11,7 @@ import {PostAuthor} from "./entity/PostAuthor";
const options: ConnectionOptions = {
driver: {
type: "postgres",
host: "192.168.99.100",
host: "localhost",
port: 5432,
username: "root",
password: "admin",

View File

@ -7,7 +7,7 @@ import {Category} from "./entity/Category";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -7,7 +7,7 @@ import {Category} from "./entity/Category";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -5,7 +5,7 @@ import {Category} from "./entity/Category";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -7,7 +7,7 @@ import {Category} from "./entity/Category";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -7,7 +7,7 @@ import {Post} from "./entity/Post";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -6,7 +6,7 @@ import {Author} from "./entity/Author";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -7,7 +7,7 @@ import {Counters} from "./entity/Counters";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -11,7 +11,7 @@ import {PostAuthor} from "./entity/PostAuthor";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -6,12 +6,15 @@ import {PostDetails} from "./entity/PostDetails";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",
database: "test"
},
logging: {
logQueries: true
},
autoSchemaCreate: true,
entityDirectories: [__dirname + "/entity/*"]
};

View File

@ -9,7 +9,7 @@ import {EverythingSubscriber} from "./subscriber/EverythingSubscriber";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -8,7 +8,7 @@ import {Blog} from "./entity/Blog";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -7,7 +7,7 @@ import {PostAuthor} from "./entity/PostAuthor";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -5,7 +5,7 @@ import {Category} from "./entity/Category";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -7,7 +7,7 @@ import {PostAuthor} from "./entity/PostAuthor";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -235,7 +235,7 @@ export class Connection {
if (dropBeforeSync)
await this.dropDatabase();
const schemaCreator = new SchemaBuilder(this.driver, this.entityMetadatas); // todo: use factory there later
const schemaCreator = new SchemaBuilder(this.driver, this.entityMetadatas, this.createNamingStrategy()); // todo: use factory there later
await schemaCreator.create();
}

View File

@ -9,6 +9,7 @@ import {PostgresDriver} from "../driver/postgres/PostgresDriver";
import {AlreadyHasActiveConnectionError} from "./error/AlreadyHasActiveConnectionError";
import {Logger} from "../logger/Logger";
import {MariaDbDriver} from "../driver/mariadb/MariaDbDriver";
import {SqliteDriver} from "../driver/sqlite/SqliteDriver";
/**
* Connection manager holds all connections made to the databases and providers helper management functions
@ -120,6 +121,8 @@ export class ConnectionManager {
return new PostgresDriver(options, logger);
case "mariadb":
return new MariaDbDriver(options, logger);
case "sqlite":
return new SqliteDriver(options, logger);
default:
throw new MissingDriverError(options.type);
}

View File

@ -9,13 +9,25 @@ import {ColumnMetadataArgs} from "../../metadata-args/ColumnMetadataArgs";
* Column decorator is used to mark a specific class property as a table column. Only properties decorated with this
* decorator will be persisted to the database when entity be saved.
*/
export function Column(options?: ColumnOptions): Function;
export function Column(): Function;
/**
* Column decorator is used to mark a specific class property as a table column. Only properties decorated with this
* decorator will be persisted to the database when entity be saved.
*/
export function Column(type?: ColumnType, options?: ColumnOptions): Function;
export function Column(type: ColumnType): Function;
/**
* Column decorator is used to mark a specific class property as a table column. Only properties decorated with this
* decorator will be persisted to the database when entity be saved.
*/
export function Column(options: ColumnOptions): Function;
/**
* Column decorator is used to mark a specific class property as a table column. Only properties decorated with this
* decorator will be persisted to the database when entity be saved.
*/
export function Column(type: ColumnType, options: ColumnOptions): Function;
/**
* Column decorator is used to mark a specific class property as a table column. Only properties decorated with this

View File

@ -6,7 +6,7 @@ export interface DriverOptions {
/**
* Database type. Mysql and postgres are the only drivers supported at this moment.
*/
readonly type: "mysql"|"postgres"|"mariadb";
readonly type: "mysql"|"postgres"|"mariadb"|"sqlite";
/**
* Url to where perform connection.
@ -38,6 +38,12 @@ export interface DriverOptions {
*/
readonly database?: string;
/**
* Storage type or path to the storage.
* Used only for SQLite.
*/
readonly storage?: string;
/**
* Indicates if connection pooling should be used or not.
* Be default it is enabled. Set to false to disable it.

View File

@ -4,6 +4,9 @@ import {ColumnSchema} from "../schema-builder/ColumnSchema";
import {ColumnMetadata} from "../metadata/ColumnMetadata";
import {TableMetadata} from "../metadata/TableMetadata";
import {TableSchema} from "../schema-builder/TableSchema";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategyInterface";
import {EntityMetadata} from "../metadata/EntityMetadata";
import {ForeignKeySchema} from "../schema-builder/ForeignKeySchema";
/**
* Runs queries on a single database connection.
@ -70,37 +73,38 @@ export interface QueryRunner {
/**
* Loads all tables (with given names) from the database and creates a TableSchema from them.
*/
loadSchemaTables(tableNames: string[]): Promise<TableSchema[]>;
loadSchemaTables(tableNames: string[], namingStrategy: NamingStrategyInterface): Promise<TableSchema[]>;
/**
* Creates a new table from the given table metadata and column metadatas.
* Returns array of created columns. This is required because some driver may not create all columns.
*/
createTable(table: TableMetadata, columns: ColumnMetadata[]): Promise<void>;
createTable(table: TableMetadata, columns: ColumnMetadata[]): Promise<ColumnMetadata[]>;
/**
* Creates a new column from the column metadata in the table.
* Creates new columns in the table.
*/
createColumn(tableName: string, column: ColumnMetadata): Promise<void>;
createColumns(tableSchema: TableSchema, columns: ColumnMetadata[]): Promise<ColumnMetadata[]>;
/**
* Changes a column in the table.
* Changes a columns in the table.
*/
changeColumn(tableName: string, oldColumn: ColumnSchema, newColumn: ColumnMetadata): Promise<void>;
changeColumns(tableSchema: TableSchema, changedColumns: { newColumn: ColumnMetadata, oldColumn: ColumnSchema }[]): Promise<void>;
/**
* Drops the column in the table.
* Drops the columns in the table.
*/
dropColumn(tableName: string, columnName: string): Promise<void>;
dropColumns(dbTable: TableSchema, columns: ColumnSchema[]): Promise<void>;
/**
* Creates a new foreign.
* Creates a new foreign keys.
*/
createForeignKey(foreignKey: ForeignKeyMetadata): Promise<void>;
createForeignKeys(dbTable: TableSchema, foreignKeys: ForeignKeySchema[]): Promise<void>;
/**
* Drops a foreign key from the table.
* Drops a foreign keys from the table.
*/
dropForeignKey(tableName: string, foreignKeyName: string): Promise<void>;
dropForeignKeys(tableSchema: TableSchema, foreignKeys: ForeignKeySchema[]): Promise<void>;
/**
* Creates a new index.

View File

@ -18,6 +18,8 @@ import {PrimaryKeySchema} from "../../schema-builder/PrimaryKeySchema";
import {IndexSchema} from "../../schema-builder/IndexSchema";
import {QueryRunnerAlreadyReleasedError} from "../error/QueryRunnerAlreadyReleasedError";
import {ColumnTypes} from "../../metadata/types/ColumnTypes";
import {NamingStrategyInterface} from "../../naming-strategy/NamingStrategyInterface";
import {EntityMetadata} from "../../metadata/EntityMetadata";
/**
* Runs queries on a single MariaDB database connection.
@ -236,11 +238,12 @@ export class MariaDbQueryRunner implements QueryRunner {
/**
* Loads all tables (with given names) from the database and creates a TableSchema from them.
*/
async loadSchemaTables(tableNames: string[]): Promise<TableSchema[]> {
async loadSchemaTables(tableNames: string[], namingStrategy: NamingStrategyInterface): Promise<TableSchema[]> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
// if no tables given then no need to proceed
if (!tableNames)
return [];
@ -291,7 +294,7 @@ export class MariaDbQueryRunner implements QueryRunner {
// create foreign key schemas from the loaded indices
tableSchema.foreignKeys = dbForeignKeys
.filter(dbForeignKey => dbForeignKey["TABLE_NAME"] === tableSchema.name)
.map(dbForeignKey => new ForeignKeySchema(dbForeignKey["CONSTRAINT_NAME"]));
.map(dbForeignKey => new ForeignKeySchema(dbForeignKey["CONSTRAINT_NAME"], [], [], "", "")); // todo: fix missing params
// create unique key schemas from the loaded indices
tableSchema.uniqueKeys = dbUniqueKeys
@ -322,73 +325,93 @@ export class MariaDbQueryRunner implements QueryRunner {
/**
* Creates a new table from the given table metadata and column metadatas.
*/
async createTable(table: TableMetadata, columns: ColumnMetadata[]): Promise<void> {
async createTable(table: TableMetadata, columns: ColumnMetadata[]): Promise<ColumnMetadata[]> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const columnDefinitions = columns.map(column => this.buildCreateColumnSql(column, false)).join(", ");
const sql = `CREATE TABLE \`${table.name}\` (${columnDefinitions}) ENGINE=InnoDB;`;
await this.query(sql);
return columns;
}
/**
* Creates a new column from the column metadata in the table.
*/
async createColumn(tableName: string, column: ColumnMetadata): Promise<void> {
async createColumns(tableSchema: TableSchema, columns: ColumnMetadata[]): Promise<ColumnMetadata[]> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const sql = `ALTER TABLE \`${tableName}\` ADD ${this.buildCreateColumnSql(column, false)}`;
await this.query(sql);
const queries = columns.map(column => {
const sql = `ALTER TABLE \`${tableSchema.name}\` ADD ${this.buildCreateColumnSql(column, false)}`;
return this.query(sql);
});
await Promise.all(queries);
return columns;
}
/**
* Changes a column in the table.
*/
async changeColumn(tableName: string, oldColumn: ColumnSchema, newColumn: ColumnMetadata): Promise<void> {
async changeColumns(tableSchema: TableSchema, changedColumns: { newColumn: ColumnMetadata, oldColumn: ColumnSchema }[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const sql = `ALTER TABLE \`${tableName}\` CHANGE \`${oldColumn.name}\` ${this.buildCreateColumnSql(newColumn, oldColumn.isPrimary)}`; // todo: CHANGE OR MODIFY COLUMN ????
await this.query(sql);
const updatePromises = changedColumns.map(changedColumn => {
const sql = `ALTER TABLE \`${tableSchema.name}\` CHANGE \`${changedColumn.oldColumn.name}\` ${this.buildCreateColumnSql(changedColumn.newColumn, changedColumn.oldColumn.isPrimary)}`; // todo: CHANGE OR MODIFY COLUMN ????
return this.query(sql);
});
await Promise.all(updatePromises);
}
/**
* Drops the column in the table.
* Drops the columns in the table.
*/
async dropColumn(tableName: string, columnName: string): Promise<void> {
async dropColumns(dbTable: TableSchema, columns: ColumnSchema[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const sql = `ALTER TABLE \`${tableName}\` DROP \`${columnName}\``;
await this.query(sql);
const dropPromises = columns.map(column => {
return this.query(`ALTER TABLE \`${dbTable.name}\` DROP \`${column.name}\``);
});
await Promise.all(dropPromises);
}
/**
* Creates a new foreign.
* Creates a new foreign keys.
*/
async createForeignKey(foreignKey: ForeignKeyMetadata): Promise<void> {
async createForeignKeys(dbTable: TableSchema, foreignKeys: ForeignKeySchema[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const columnNames = foreignKey.columnNames.map(column => "`" + column + "`").join(", ");
const referencedColumnNames = foreignKey.referencedColumnNames.map(column => "`" + column + "`").join(",");
let sql = `ALTER TABLE ${foreignKey.tableName} ADD CONSTRAINT \`${foreignKey.name}\` ` +
`FOREIGN KEY (${columnNames}) ` +
`REFERENCES \`${foreignKey.referencedTable.name}\`(${referencedColumnNames})`;
if (foreignKey.onDelete) sql += " ON DELETE " + foreignKey.onDelete;
await this.query(sql);
const promises = foreignKeys.map(foreignKey => {
const columnNames = foreignKey.columnNames.map(column => "`" + column + "`").join(", ");
const referencedColumnNames = foreignKey.referencedColumnNames.map(column => "`" + column + "`").join(",");
let sql = `ALTER TABLE ${dbTable.name} ADD CONSTRAINT \`${foreignKey.name}\` ` +
`FOREIGN KEY (${columnNames}) ` +
`REFERENCES \`${foreignKey.referencedTableName}\`(${referencedColumnNames})`;
if (foreignKey.onDelete) sql += " ON DELETE " + foreignKey.onDelete;
return this.query(sql);
});
await Promise.all(promises);
}
/**
* Drops a foreign key from the table.
* Drops a foreign keys from the table.
*/
async dropForeignKey(tableName: string, foreignKeyName: string): Promise<void> {
async dropForeignKeys(tableSchema: TableSchema, foreignKeys: ForeignKeySchema[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const sql = `ALTER TABLE \`${tableName}\` DROP FOREIGN KEY \`${foreignKeyName}\``;
await this.query(sql);
const promises = foreignKeys.map(foreignKey => {
const sql = `ALTER TABLE \`${tableSchema.name}\` DROP FOREIGN KEY \`${foreignKey.name}\``;
return this.query(sql);
});
await Promise.all(promises);
}
/**

View File

@ -17,6 +17,8 @@ import {ForeignKeySchema} from "../../schema-builder/ForeignKeySchema";
import {PrimaryKeySchema} from "../../schema-builder/PrimaryKeySchema";
import {IndexSchema} from "../../schema-builder/IndexSchema";
import {QueryRunnerAlreadyReleasedError} from "../error/QueryRunnerAlreadyReleasedError";
import {NamingStrategyInterface} from "../../naming-strategy/NamingStrategyInterface";
import {EntityMetadata} from "../../metadata/EntityMetadata";
/**
* Runs queries on a single mysql database connection.
@ -217,11 +219,12 @@ export class MysqlQueryRunner implements QueryRunner {
/**
* Loads all tables (with given names) from the database and creates a TableSchema from them.
*/
async loadSchemaTables(tableNames: string[]): Promise<TableSchema[]> {
async loadSchemaTables(tableNames: string[], namingStrategy: NamingStrategyInterface): Promise<TableSchema[]> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
// if no tables given then no need to proceed
if (!tableNames)
return [];
@ -272,7 +275,7 @@ export class MysqlQueryRunner implements QueryRunner {
// create foreign key schemas from the loaded indices
tableSchema.foreignKeys = dbForeignKeys
.filter(dbForeignKey => dbForeignKey["TABLE_NAME"] === tableSchema.name)
.map(dbForeignKey => new ForeignKeySchema(dbForeignKey["CONSTRAINT_NAME"]));
.map(dbForeignKey => new ForeignKeySchema(dbForeignKey["CONSTRAINT_NAME"], [], [], "", "")); // todo: fix missing params
// create unique key schemas from the loaded indices
tableSchema.uniqueKeys = dbUniqueKeys
@ -303,73 +306,93 @@ export class MysqlQueryRunner implements QueryRunner {
/**
* Creates a new table from the given table metadata and column metadatas.
*/
async createTable(table: TableMetadata, columns: ColumnMetadata[]): Promise<void> {
async createTable(table: TableMetadata, columns: ColumnMetadata[]): Promise<ColumnMetadata[]> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const columnDefinitions = columns.map(column => this.buildCreateColumnSql(column, false)).join(", ");
const sql = `CREATE TABLE \`${table.name}\` (${columnDefinitions}) ENGINE=InnoDB;`;
await this.query(sql);
return columns;
}
/**
* Creates a new column from the column metadata in the table.
*/
async createColumn(tableName: string, column: ColumnMetadata): Promise<void> {
async createColumns(tableSchema: TableSchema, columns: ColumnMetadata[]): Promise<ColumnMetadata[]> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const sql = `ALTER TABLE \`${tableName}\` ADD ${this.buildCreateColumnSql(column, false)}`;
await this.query(sql);
const queries = columns.map(column => {
const sql = `ALTER TABLE \`${tableSchema.name}\` ADD ${this.buildCreateColumnSql(column, false)}`;
return this.query(sql);
});
await Promise.all(queries);
return columns;
}
/**
* Changes a column in the table.
*/
async changeColumn(tableName: string, oldColumn: ColumnSchema, newColumn: ColumnMetadata): Promise<void> {
async changeColumns(tableSchema: TableSchema, changedColumns: { newColumn: ColumnMetadata, oldColumn: ColumnSchema }[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const sql = `ALTER TABLE \`${tableName}\` CHANGE \`${oldColumn.name}\` ${this.buildCreateColumnSql(newColumn, oldColumn.isPrimary)}`; // todo: CHANGE OR MODIFY COLUMN ????
await this.query(sql);
const updatePromises = changedColumns.map(changedColumn => {
const sql = `ALTER TABLE \`${tableSchema.name}\` CHANGE \`${changedColumn.oldColumn.name}\` ${this.buildCreateColumnSql(changedColumn.newColumn, changedColumn.oldColumn.isPrimary)}`; // todo: CHANGE OR MODIFY COLUMN ????
return this.query(sql);
});
await Promise.all(updatePromises);
}
/**
* Drops the column in the table.
* Drops the columns in the table.
*/
async dropColumn(tableName: string, columnName: string): Promise<void> {
async dropColumns(dbTable: TableSchema, columns: ColumnSchema[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const sql = `ALTER TABLE \`${tableName}\` DROP \`${columnName}\``;
await this.query(sql);
const dropPromises = columns.map(column => {
return this.query(`ALTER TABLE \`${dbTable.name}\` DROP \`${column.name}\``);
});
await Promise.all(dropPromises);
}
/**
* Creates a new foreign.
* Creates a new foreign keys.
*/
async createForeignKey(foreignKey: ForeignKeyMetadata): Promise<void> {
async createForeignKeys(dbTable: TableSchema, foreignKeys: ForeignKeySchema[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const columnNames = foreignKey.columnNames.map(column => "`" + column + "`").join(", ");
const referencedColumnNames = foreignKey.referencedColumnNames.map(column => "`" + column + "`").join(",");
let sql = `ALTER TABLE ${foreignKey.tableName} ADD CONSTRAINT \`${foreignKey.name}\` ` +
`FOREIGN KEY (${columnNames}) ` +
`REFERENCES \`${foreignKey.referencedTable.name}\`(${referencedColumnNames})`;
if (foreignKey.onDelete) sql += " ON DELETE " + foreignKey.onDelete;
await this.query(sql);
const promises = foreignKeys.map(foreignKey => {
const columnNames = foreignKey.columnNames.map(column => "`" + column + "`").join(", ");
const referencedColumnNames = foreignKey.referencedColumnNames.map(column => "`" + column + "`").join(",");
let sql = `ALTER TABLE ${dbTable.name} ADD CONSTRAINT \`${foreignKey.name}\` ` +
`FOREIGN KEY (${columnNames}) ` +
`REFERENCES \`${foreignKey.referencedTableName}\`(${referencedColumnNames})`;
if (foreignKey.onDelete) sql += " ON DELETE " + foreignKey.onDelete;
return this.query(sql);
});
await Promise.all(promises);
}
/**
* Drops a foreign key from the table.
* Drops a foreign keys from the table.
*/
async dropForeignKey(tableName: string, foreignKeyName: string): Promise<void> {
async dropForeignKeys(tableSchema: TableSchema, foreignKeys: ForeignKeySchema[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const sql = `ALTER TABLE \`${tableName}\` DROP FOREIGN KEY \`${foreignKeyName}\``;
await this.query(sql);
const promises = foreignKeys.map(foreignKey => {
const sql = `ALTER TABLE \`${tableSchema.name}\` DROP FOREIGN KEY \`${foreignKey.name}\``;
return this.query(sql);
});
await Promise.all(promises);
}
/**

View File

@ -17,9 +17,11 @@ import {ForeignKeySchema} from "../../schema-builder/ForeignKeySchema";
import {PrimaryKeySchema} from "../../schema-builder/PrimaryKeySchema";
import {UniqueKeySchema} from "../../schema-builder/UniqueKeySchema";
import {QueryRunnerAlreadyReleasedError} from "../error/QueryRunnerAlreadyReleasedError";
import {NamingStrategyInterface} from "../../naming-strategy/NamingStrategyInterface";
import {EntityMetadata} from "../../metadata/EntityMetadata";
/**
* Runs queries on a single mysql database connection.
* Runs queries on a single postgres database connection.
*/
export class PostgresQueryRunner implements QueryRunner {
@ -213,12 +215,12 @@ export class PostgresQueryRunner implements QueryRunner {
/**
* Loads all tables (with given names) from the database and creates a TableSchema from them.
*/
async loadSchemaTables(tableNames: string[]): Promise<TableSchema[]> {
async loadSchemaTables(tableNames: string[], namingStrategy: NamingStrategyInterface): Promise<TableSchema[]> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
// if no tables given then no need to proceed
if (!tableNames)
return [];
@ -277,7 +279,7 @@ export class PostgresQueryRunner implements QueryRunner {
// create foreign key schemas from the loaded indices
tableSchema.foreignKeys = dbForeignKeys
.filter(dbForeignKey => dbForeignKey["table_name"] === tableSchema.name)
.map(dbForeignKey => new ForeignKeySchema(dbForeignKey["constraint_name"]));
.map(dbForeignKey => new ForeignKeySchema(dbForeignKey["constraint_name"], [], [], "", "")); // todo: fix missing params
// create unique key schemas from the loaded indices
tableSchema.uniqueKeys = dbUniqueKeys
@ -308,109 +310,130 @@ export class PostgresQueryRunner implements QueryRunner {
/**
* Creates a new table from the given table metadata and column metadatas.
*/
async createTable(table: TableMetadata, columns: ColumnMetadata[]): Promise<void> {
async createTable(table: TableMetadata, columns: ColumnMetadata[]): Promise<ColumnMetadata[]> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const columnDefinitions = columns.map(column => this.buildCreateColumnSql(column, false)).join(", ");
const sql = `CREATE TABLE "${table.name}" (${columnDefinitions})`;
await this.query(sql);
return columns;
}
/**
* Creates a new column from the column metadata in the table.
*/
async createColumn(tableName: string, column: ColumnMetadata): Promise<void> {
async createColumns(tableSchema: TableSchema, columns: ColumnMetadata[]): Promise<ColumnMetadata[]> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const sql = `ALTER TABLE "${tableName}" ADD ${this.buildCreateColumnSql(column, false)}`;
await this.query(sql);
const queries = columns.map(column => {
const sql = `ALTER TABLE "${tableSchema.name}" ADD ${this.buildCreateColumnSql(column, false)}`;
return this.query(sql);
});
await Promise.all(queries);
return columns;
}
/**
* Changes a column in the table.
*/
async changeColumn(tableName: string, oldColumn: ColumnSchema, newColumn: ColumnMetadata): Promise<void> {
async changeColumns(tableSchema: TableSchema, changedColumns: { newColumn: ColumnMetadata, oldColumn: ColumnSchema }[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
// update name, type, nullable
const newType = this.normalizeType(newColumn);
if (oldColumn.type !== newType ||
oldColumn.name !== newColumn.name) {
const updatePromises = changedColumns.map(async changedColumn => {
const oldColumn = changedColumn.oldColumn;
const newColumn = changedColumn.newColumn;
const newType = this.normalizeType(newColumn);
let sql = `ALTER TABLE "${tableName}" ALTER COLUMN "${oldColumn.name}"`;
if (oldColumn.type !== newType) {
sql += ` TYPE ${newType}`;
}
if (oldColumn.name !== newColumn.name) { // todo: make rename in a separate query too
sql += ` RENAME TO ` + newColumn.name;
}
await this.query(sql);
}
if (oldColumn.type !== newType ||
oldColumn.name !== newColumn.name) {
if (oldColumn.isNullable !== newColumn.isNullable) {
let sql = `ALTER TABLE "${tableName}" ALTER COLUMN "${oldColumn.name}"`;
if (newColumn.isNullable) {
sql += ` DROP NOT NULL`;
} else {
sql += ` SET NOT NULL`;
let sql = `ALTER TABLE "${tableSchema.name}" ALTER COLUMN "${oldColumn.name}"`;
if (oldColumn.type !== newType) {
sql += ` TYPE ${newType}`;
}
if (oldColumn.name !== newColumn.name) { // todo: make rename in a separate query too
sql += ` RENAME TO ` + newColumn.name;
}
await this.query(sql);
}
await this.query(sql);
}
// update sequence generation
if (oldColumn.isGenerated !== newColumn.isGenerated) {
if (!oldColumn.isGenerated) {
await this.query(`CREATE SEQUENCE "${tableName}_id_seq" OWNED BY ${tableName}.${oldColumn.name}`);
await this.query(`ALTER TABLE "${tableName}" ALTER COLUMN "${oldColumn.name}" SET DEFAULT nextval('"${tableName}_id_seq"')`);
} else {
await this.query(`ALTER TABLE "${tableName}" ALTER COLUMN "${oldColumn.name}" DROP DEFAULT`);
await this.query(`DROP SEQUENCE "${tableName}_id_seq"`);
if (oldColumn.isNullable !== newColumn.isNullable) {
let sql = `ALTER TABLE "${tableSchema.name}" ALTER COLUMN "${oldColumn.name}"`;
if (newColumn.isNullable) {
sql += ` DROP NOT NULL`;
} else {
sql += ` SET NOT NULL`;
}
await this.query(sql);
}
}
if (oldColumn.comment !== newColumn.comment) {
await this.query(`COMMENT ON COLUMN "${tableName}"."${oldColumn.name}" is '${newColumn.comment}'`);
}
// update sequence generation
if (oldColumn.isGenerated !== newColumn.isGenerated) {
if (!oldColumn.isGenerated) {
await this.query(`CREATE SEQUENCE "${tableSchema.name}_id_seq" OWNED BY ${tableSchema.name}.${oldColumn.name}`);
await this.query(`ALTER TABLE "${tableSchema.name}" ALTER COLUMN "${oldColumn.name}" SET DEFAULT nextval('"${tableSchema.name}_id_seq"')`);
} else {
await this.query(`ALTER TABLE "${tableSchema.name}" ALTER COLUMN "${oldColumn.name}" DROP DEFAULT`);
await this.query(`DROP SEQUENCE "${tableSchema.name}_id_seq"`);
}
}
if (oldColumn.comment !== newColumn.comment) {
await this.query(`COMMENT ON COLUMN "${tableSchema.name}"."${oldColumn.name}" is '${newColumn.comment}'`);
}
});
await Promise.all(updatePromises);
}
/**
* Drops the column in the table.
* Drops the columns in the table.
*/
async dropColumn(tableName: string, columnName: string): Promise<void> {
async dropColumns(dbTable: TableSchema, columns: ColumnSchema[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const sql = `ALTER TABLE "${tableName}" DROP "${columnName}"`;
await this.query(sql);
const dropPromises = columns.map(column => {
return this.query(`ALTER TABLE "${dbTable.name}" DROP "${column.name}"`);
});
await Promise.all(dropPromises);
}
/**
* Creates a new foreign.
* Creates a new foreign keys.
*/
async createForeignKey(foreignKey: ForeignKeyMetadata): Promise<void> {
async createForeignKeys(dbTable: TableSchema, foreignKeys: ForeignKeySchema[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
let sql = `ALTER TABLE "${foreignKey.tableName}" ADD CONSTRAINT "${foreignKey.name}" ` +
`FOREIGN KEY ("${foreignKey.columnNames.join("\", \"")}") ` +
`REFERENCES "${foreignKey.referencedTable.name}"("${foreignKey.referencedColumnNames.join("\", \"")}")`;
if (foreignKey.onDelete)
sql += " ON DELETE " + foreignKey.onDelete;
await this.query(sql);
const promises = foreignKeys.map(foreignKey => {
let sql = `ALTER TABLE "${dbTable.name}" ADD CONSTRAINT "${foreignKey.name}" ` +
`FOREIGN KEY ("${foreignKey.columnNames.join("\", \"")}") ` +
`REFERENCES "${foreignKey.referencedTableName}"("${foreignKey.referencedColumnNames.join("\", \"")}")`;
if (foreignKey.onDelete) sql += " ON DELETE " + foreignKey.onDelete;
return this.query(sql);
});
await Promise.all(promises);
}
/**
* Drops a foreign key from the table.
* Drops a foreign keys from the table.
*/
async dropForeignKey(tableName: string, foreignKeyName: string): Promise<void> {
async dropForeignKeys(tableSchema: TableSchema, foreignKeys: ForeignKeySchema[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const sql = `ALTER TABLE "${tableName}" DROP CONSTRAINT "${foreignKeyName}"`;
await this.query(sql);
const promises = foreignKeys.map(foreignKey => {
const sql = `ALTER TABLE "${tableSchema.name}" DROP CONSTRAINT "${foreignKey.name}"`;
return this.query(sql);
});
await Promise.all(promises);
}
/**
@ -453,7 +476,7 @@ export class PostgresQueryRunner implements QueryRunner {
/**
* Creates a database type from a given column metadata.
*/
normalizeType(column: ColumnMetadata) {
normalizeType(column: ColumnMetadata): string {
switch (column.normalizedDataType) {
case "string":
return "character varying(" + (column.length ? column.length : 255) + ")";

View File

@ -0,0 +1,271 @@
import {Driver} from "../Driver";
import {ConnectionIsNotSetError} from "../error/ConnectionIsNotSetError";
import {DriverOptions} from "../DriverOptions";
import {ObjectLiteral} from "../../common/ObjectLiteral";
import {DatabaseConnection} from "../DatabaseConnection";
import {DriverPackageNotInstalledError} from "../error/DriverPackageNotInstalledError";
import {DriverPackageLoadError} from "../error/DriverPackageLoadError";
import {DriverUtils} from "../DriverUtils";
import {ColumnTypes, ColumnType} from "../../metadata/types/ColumnTypes";
import {ColumnMetadata} from "../../metadata/ColumnMetadata";
import {Logger} from "../../logger/Logger";
import * as moment from "moment";
import {SqliteQueryRunner} from "./SqliteQueryRunner";
import {QueryRunner} from "../QueryRunner";
import {DriverOptionNotSetError} from "../error/DriverOptionNotSetError";
import {NamingStrategyInterface} from "../../naming-strategy/NamingStrategyInterface";
/**
* Organizes communication with sqlite DBMS.
*/
export class SqliteDriver implements Driver {
// -------------------------------------------------------------------------
// Public Properties
// -------------------------------------------------------------------------
/**
* Driver connection options.
*/
readonly options: DriverOptions;
// -------------------------------------------------------------------------
// Protected Properties
// -------------------------------------------------------------------------
/**
* SQLite library.
*/
protected sqlite: any;
/**
* Connection to SQLite database.
*/
protected databaseConnection: DatabaseConnection|undefined;
/**
* Logger used go log queries and errors.
*/
protected logger: Logger;
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(connectionOptions: DriverOptions, logger: Logger, sqlite?: any) {
this.options = connectionOptions;
this.logger = logger;
this.sqlite = sqlite;
// validate options to make sure everything is set
if (!this.options.storage)
throw new DriverOptionNotSetError("storage");
// if sqlite package instance was not set explicitly then try to load it
if (!sqlite)
this.loadDependencies();
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
/**
* Performs connection to the database.
*/
connect(): Promise<void> {
return new Promise<void>((ok, fail) => {
const connection = new this.sqlite.Database(this.options.storage, (err: any) => {
if (err)
return fail(err);
this.databaseConnection = {
id: 1,
connection: connection,
isTransactionActive: false
};
ok();
});
});
}
/**
* Closes connection with database.
*/
disconnect(): Promise<void> {
return new Promise<void>((ok, fail) => {
const handler = (err: any) => err ? fail(err) : ok();
if (!this.databaseConnection)
return fail(new ConnectionIsNotSetError("sqlite"));
this.databaseConnection.connection.close(handler);
});
}
/**
* Creates a query runner used for common queries.
*/
async createQueryRunner(): Promise<QueryRunner> {
if (!this.databaseConnection)
return Promise.reject(new ConnectionIsNotSetError("sqlite"));
const databaseConnection = await this.retrieveDatabaseConnection();
return new SqliteQueryRunner(databaseConnection, this, this.logger);
}
/**
* Access to the native implementation of the database.
*/
nativeInterface() {
return {
driver: this.sqlite,
connection: this.databaseConnection ? this.databaseConnection.connection : undefined
};
}
/**
* Prepares given value to a value to be persisted, based on its column type and metadata.
*/
preparePersistentValue(value: any, column: ColumnMetadata): any {
switch (column.type) {
case ColumnTypes.BOOLEAN:
return value === true ? 1 : 0;
case ColumnTypes.DATE:
return moment(value).format("YYYY-MM-DD");
case ColumnTypes.TIME:
return moment(value).format("HH:mm:ss");
case ColumnTypes.DATETIME:
return moment(value).format("YYYY-MM-DD HH:mm:ss");
case ColumnTypes.JSON:
return JSON.stringify(value);
case ColumnTypes.SIMPLE_ARRAY:
return (value as any[])
.map(i => String(i))
.join(",");
}
return value;
}
/**
* Prepares given value to a value to be persisted, based on its column metadata.
*/
prepareHydratedValue(value: any, type: ColumnType): any;
/**
* Prepares given value to a value to be persisted, based on its column type.
*/
prepareHydratedValue(value: any, column: ColumnMetadata): any;
/**
* Prepares given value to a value to be persisted, based on its column type or metadata.
*/
prepareHydratedValue(value: any, columnOrColumnType: ColumnMetadata|ColumnType): any {
const type = columnOrColumnType instanceof ColumnMetadata ? columnOrColumnType.type : columnOrColumnType;
switch (type) {
case ColumnTypes.BOOLEAN:
return value ? true : false;
case ColumnTypes.DATE:
if (value instanceof Date)
return value;
return moment(value, "YYYY-MM-DD").toDate();
case ColumnTypes.TIME:
return moment(value, "HH:mm:ss").toDate();
case ColumnTypes.DATETIME:
if (value instanceof Date)
return value;
return moment(value, "YYYY-MM-DD HH:mm:ss").toDate();
case ColumnTypes.JSON:
return JSON.parse(value);
case ColumnTypes.SIMPLE_ARRAY:
return (value as string).split(",");
}
return value;
}
/**
* Replaces parameters in the given sql with special escaping character
* and an array of parameter names to be passed to a query.
*/
escapeQueryWithParameters(sql: string, parameters: ObjectLiteral): [string, any[]] {
if (!parameters || !Object.keys(parameters).length)
return [sql, []];
const builtParameters: any[] = [];
const keys = Object.keys(parameters).map(parameter => "(:" + parameter + "\\b)").join("|");
sql = sql.replace(new RegExp(keys, "g"), (key: string): string => {
const value = parameters[key.substr(1)];
if (value instanceof Array) {
return value.map((v: any) => {
builtParameters.push(v);
return "$" + builtParameters.length;
}).join(", ");
} else {
builtParameters.push(value);
}
return "$" + builtParameters.length;
}); // todo: make replace only in value statements, otherwise problems
return [sql, builtParameters];
}
/**
* Escapes a column name.
*/
escapeColumnName(columnName: string): string {
return "\"" + columnName + "\"";
}
/**
* Escapes an alias.
*/
escapeAliasName(aliasName: string): string {
return "\"" + aliasName + "\"";
}
/**
* Escapes a table name.
*/
escapeTableName(tableName: string): string {
return "\"" + tableName + "\"";
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
/**
* Retrieves a new database connection.
* If pooling is enabled then connection from the pool will be retrieved.
* Otherwise active connection will be returned.
*/
protected retrieveDatabaseConnection(): Promise<DatabaseConnection> {
if (this.databaseConnection)
return Promise.resolve(this.databaseConnection);
throw new ConnectionIsNotSetError("sqlite");
}
/**
* If driver dependency is not given explicitly, then try to load it via "require".
*/
protected loadDependencies(): void {
if (!require)
throw new DriverPackageLoadError();
try {
this.sqlite = require("sqlite3").verbose();
} catch (e) {
throw new DriverPackageNotInstalledError("SQLite", "sqlite3");
}
}
}

View File

@ -0,0 +1,591 @@
import {QueryRunner} from "../QueryRunner";
import {ObjectLiteral} from "../../common/ObjectLiteral";
import {Logger} from "../../logger/Logger";
import {DatabaseConnection} from "../DatabaseConnection";
import {TransactionAlreadyStartedError} from "../error/TransactionAlreadyStartedError";
import {TransactionNotStartedError} from "../error/TransactionNotStartedError";
import {SqliteDriver} from "./SqliteDriver";
import {DataTypeNotSupportedByDriverError} from "../error/DataTypeNotSupportedByDriverError";
import {IndexMetadata} from "../../metadata/IndexMetadata";
import {ColumnSchema} from "../../schema-builder/ColumnSchema";
import {ColumnMetadata} from "../../metadata/ColumnMetadata";
import {TableMetadata} from "../../metadata/TableMetadata";
import {TableSchema} from "../../schema-builder/TableSchema";
import {IndexSchema} from "../../schema-builder/IndexSchema";
import {ForeignKeySchema} from "../../schema-builder/ForeignKeySchema";
import {PrimaryKeySchema} from "../../schema-builder/PrimaryKeySchema";
import {UniqueKeySchema} from "../../schema-builder/UniqueKeySchema";
import {QueryRunnerAlreadyReleasedError} from "../error/QueryRunnerAlreadyReleasedError";
import {NamingStrategyInterface} from "../../naming-strategy/NamingStrategyInterface";
/**
* Runs queries on a single sqlite database connection.
*/
export class SqliteQueryRunner implements QueryRunner {
// -------------------------------------------------------------------------
// Protected Properties
// -------------------------------------------------------------------------
/**
* Indicates if connection for this query runner is released.
* Once its released, query runner cannot run queries anymore.
*/
protected isReleased = false;
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(protected databaseConnection: DatabaseConnection,
protected driver: SqliteDriver,
protected logger: Logger) {
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
/**
* Releases database connection. This is needed when using connection pooling.
* If connection is not from a pool, it should not be released.
*/
release(): Promise<void> {
if (this.databaseConnection.releaseCallback) {
this.isReleased = true;
return this.databaseConnection.releaseCallback();
}
return Promise.resolve();
}
/**
* Removes all tables from the currently connected database.
*/
async clearDatabase(): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const selectDropsQuery = `select 'drop table ' || name || ';' as query from sqlite_master where type = 'table' and name != 'sqlite_sequence'`;
const dropQueries: ObjectLiteral[] = await this.query(selectDropsQuery);
await Promise.all(dropQueries.map(q => this.query(q["query"])));
}
/**
* Starts transaction.
*/
async beginTransaction(): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
if (this.databaseConnection.isTransactionActive)
throw new TransactionAlreadyStartedError();
await this.query("BEGIN TRANSACTION");
this.databaseConnection.isTransactionActive = true;
}
/**
* Commits transaction.
*/
async commitTransaction(): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
if (!this.databaseConnection.isTransactionActive)
throw new TransactionNotStartedError();
await this.query("COMMIT");
this.databaseConnection.isTransactionActive = false;
}
/**
* Rollbacks transaction.
*/
async rollbackTransaction(): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
if (!this.databaseConnection.isTransactionActive)
throw new TransactionNotStartedError();
await this.query("ROLLBACK");
this.databaseConnection.isTransactionActive = false;
}
/**
* Checks if transaction is in progress.
*/
isTransactionActive(): boolean {
return this.databaseConnection.isTransactionActive;
}
/**
* Executes a given SQL query.
*/
query(query: string, parameters?: any[]): Promise<any> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
// console.log("query: ", query);
// console.log("parameters: ", parameters);
this.logger.logQuery(query);
return new Promise<any[]>((ok, fail) => {
this.databaseConnection.connection.all(query, parameters, (err: any, result: any) => {
if (err) {
this.logger.logFailedQuery(query);
this.logger.logQueryError(err);
fail(err);
} else {
ok(result);
}
});
});
}
/**
* Insert a new row into given table.
*/
async insert(tableName: string, keyValues: ObjectLiteral, idColumn?: ColumnMetadata): Promise<any> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const keys = Object.keys(keyValues);
const columns = keys.map(key => this.driver.escapeColumnName(key)).join(", ");
const values = keys.map((key, index) => "$" + (index + 1)).join(",");
const sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) VALUES (${values})`;
const parameters = keys.map(key => keyValues[key]);
// console.log("query: ", sql);
// console.log("parameters: ", parameters);
this.logger.logQuery(sql);
return new Promise<any[]>((ok, fail) => {
const _this = this;
this.databaseConnection.connection.run(sql, parameters, function (err: any): void {
if (err) {
_this.logger.logFailedQuery(sql);
_this.logger.logQueryError(err);
fail(err);
} else {
if (idColumn)
return ok(this["lastID"]);
ok();
}
});
});
}
/**
* Updates rows that match given conditions in the given table.
*/
async update(tableName: string, valuesMap: ObjectLiteral, conditions: ObjectLiteral): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const updateValues = this.parametrize(valuesMap).join(", ");
const conditionString = this.parametrize(conditions, Object.keys(valuesMap).length).join(" AND ");
const query = `UPDATE ${this.driver.escapeTableName(tableName)} SET ${updateValues} ${conditionString ? (" WHERE " + conditionString) : ""}`;
const updateParams = Object.keys(valuesMap).map(key => valuesMap[key]);
const conditionParams = Object.keys(conditions).map(key => conditions[key]);
const allParameters = updateParams.concat(conditionParams);
await this.query(query, allParameters);
}
/**
* Deletes from the given table by a given conditions.
*/
async delete(tableName: string, conditions: ObjectLiteral): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const conditionString = this.parametrize(conditions).join(" AND ");
const parameters = Object.keys(conditions).map(key => conditions[key]);
const query = `DELETE FROM "${tableName}" WHERE ${conditionString}`;
await this.query(query, parameters);
}
/**
* Inserts rows into closure table.
*/
async insertIntoClosureTable(tableName: string, newEntityId: any, parentId: any, hasLevel: boolean): Promise<number> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
let sql = "";
if (hasLevel) {
sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(ancestor, descendant, level) ` +
`SELECT ancestor, ${newEntityId}, level + 1 FROM ${this.driver.escapeTableName(tableName)} WHERE descendant = ${parentId} ` +
`UNION ALL SELECT ${newEntityId}, ${newEntityId}, 1`;
} else {
sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(ancestor, descendant) ` +
`SELECT ancestor, ${newEntityId} FROM ${this.driver.escapeTableName(tableName)} WHERE descendant = ${parentId} ` +
`UNION ALL SELECT ${newEntityId}, ${newEntityId}`;
}
await this.query(sql);
const results: ObjectLiteral[] = await this.query(`SELECT MAX(level) as level FROM ${tableName} WHERE descendant = ${parentId}`);
return results && results[0] && results[0]["level"] ? parseInt(results[0]["level"]) + 1 : 1;
}
/**
* Loads all tables (with given names) from the database and creates a TableSchema from them.
*/
async loadSchemaTables(tableNames: string[], namingStrategy: NamingStrategyInterface): Promise<TableSchema[]> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
// if no tables given then no need to proceed
if (!tableNames)
return [];
// load tables, columns, indices and foreign keys
const dbTables: ObjectLiteral[] = await this.query(`SELECT * FROM sqlite_master WHERE name != 'sqlite_sequence'`);
// if tables were not found in the db, no need to proceed
if (!dbTables || !dbTables.length)
return [];
// create table schemas for loaded tables
return Promise.all(dbTables.map(async dbTable => {
const tableSchema = new TableSchema(dbTable["name"]);
// load columns and indices
const [dbColumns, dbIndices, dbForeignKeys]: ObjectLiteral[][] = await Promise.all([
this.query(`PRAGMA table_info("${dbTable["name"]}")`),
this.query(`PRAGMA index_list("${dbTable["name"]}")`),
this.query(`PRAGMA foreign_key_list("${dbTable["name"]}")`),
]);
// find column name with auto increment
let autoIncrementColumnName: string|undefined = undefined;
const tableSql: string = dbTable["sql"];
if (tableSql.indexOf("AUTOINCREMENT") !== -1) {
autoIncrementColumnName = tableSql.substr(0, tableSql.indexOf("AUTOINCREMENT"));
const comma = autoIncrementColumnName.lastIndexOf(",");
const bracket = autoIncrementColumnName.lastIndexOf("(");
if (comma !== -1) {
autoIncrementColumnName = autoIncrementColumnName.substr(comma);
autoIncrementColumnName = autoIncrementColumnName.substr(0, autoIncrementColumnName.lastIndexOf("\""));
autoIncrementColumnName = autoIncrementColumnName.substr(autoIncrementColumnName.indexOf("\"") + 1);
} else if (bracket !== -1) {
autoIncrementColumnName = autoIncrementColumnName.substr(bracket);
autoIncrementColumnName = autoIncrementColumnName.substr(0, autoIncrementColumnName.lastIndexOf("\""));
autoIncrementColumnName = autoIncrementColumnName.substr(autoIncrementColumnName.indexOf("\"") + 1);
}
}
// create column schemas from the loaded columns
tableSchema.columns = dbColumns.map(dbColumn => {
const columnSchema = new ColumnSchema();
columnSchema.name = dbColumn["name"];
columnSchema.type = dbColumn["type"];
columnSchema.default = dbColumn["dflt_value"] !== null && dbColumn["dflt_value"] !== undefined ? dbColumn["dflt_value"] : undefined;
columnSchema.isNullable = dbColumn["notnull"] === 0;
columnSchema.isPrimary = dbColumn["pk"] === 1;
columnSchema.comment = ""; // todo
columnSchema.isGenerated = autoIncrementColumnName === dbColumn["name"];
const columnForeignKeys = dbForeignKeys
.filter(foreignKey => foreignKey["from"] === dbColumn["name"])
.map(foreignKey => {
const keyName = namingStrategy.foreignKeyName(dbTable["name"], [foreignKey["from"]], foreignKey["table"], [foreignKey["to"]]);
return new ForeignKeySchema(keyName, [foreignKey["from"]], [foreignKey["to"]], foreignKey["table"], foreignKey["on_delete"]); // todo: how sqlite return from and to when they are arrays? (multiple column foreign keys)
});
tableSchema.addForeignKeys(columnForeignKeys);
return columnSchema;
});
// create primary key schema
const primaryKey = dbIndices.find(index => index["origin"] === "pk");
if (primaryKey)
tableSchema.primaryKey = new PrimaryKeySchema(primaryKey["name"]);
// create foreign key schemas from the loaded indices
// tableSchema.foreignKeys = dbForeignKeys.map(dbForeignKey => {
// const keyName = namingStrategy.foreignKeyName(dbTable["name"], [dbForeignKey["from"]], dbForeignKey["table"], [dbForeignKey["to"]]);
// return new ForeignKeySchema(keyName, dbForeignKey["from"], dbForeignKey["to"], dbForeignKey["table"]);
// });
// create unique key schemas from the loaded indices
tableSchema.uniqueKeys = dbIndices
.filter(dbIndex => dbIndex["unique"] === "1")
.map(dbUniqueKey => new UniqueKeySchema(dbUniqueKey["constraint_name"]));
// create index schemas from the loaded indices
tableSchema.indices = dbIndices
.filter(dbIndex => {
return dbIndex["origin"] !== "pk" &&
(!tableSchema.foreignKeys || !tableSchema.foreignKeys.find(foreignKey => foreignKey.name === dbIndex["name"])) &&
(!tableSchema.primaryKey || tableSchema.primaryKey.name !== dbIndex["name"]);
})
.map(dbIndex => dbIndex["index_name"])
.filter((value, index, self) => self.indexOf(value) === index) // unqiue
.map(dbIndexName => {
const columnNames = dbIndices
.filter(dbIndex => dbIndex["table_name"] === tableSchema.name && dbIndex["index_name"] === dbIndexName)
.map(dbIndex => dbIndex["column_name"]);
return new IndexSchema(dbIndexName, columnNames);
});
return tableSchema;
}));
}
/**
* Creates a new table from the given table metadata and column metadatas.
*/
async createTable(table: TableMetadata, columns: ColumnMetadata[]): Promise<ColumnMetadata[]> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
// skip columns with foreign keys, we will add them later
const columnDefinitions = columns.map(column => this.buildCreateColumnSql(column, false)).join(", ");
const sql = `CREATE TABLE "${table.name}" (${columnDefinitions})`;
await this.query(sql);
return columns;
}
/**
* Creates a new column from the column metadata in the table.
*/
async createColumns(tableSchema: TableSchema, columns: ColumnMetadata[]): Promise<ColumnMetadata[]> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
// don't create columns if it has a foreign key
// if (column.foreignKeys.length > 0)
// return false;
// const withoutForeignKeyColumns = columns.filter(column => column.foreignKeys.length === 0);
const columnsSchemas = columns.map(column => ColumnSchema.create(this, column));
const dbColumns = tableSchema.columns.concat(columnsSchemas);
await this.recreateTable(tableSchema, dbColumns);
return columns;
}
/**
* Changes a column in the table.
* Changed column looses all its keys in the db.
*/
async changeColumns(tableSchema: TableSchema, changedColumns: { newColumn: ColumnMetadata, oldColumn: ColumnSchema }[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const newDbColumns = changedColumns.map(changedColumn => ColumnSchema.create(this, changedColumn.newColumn));
const oldColumns = tableSchema.columns.filter(dbColumn => {
return !!changedColumns.find(changedColumn => changedColumn.oldColumn.name === dbColumn.name);
});
const newTable = tableSchema.clone();
newTable.removeColumns(oldColumns);
newTable.addColumns(newDbColumns);
return this.recreateTable(newTable);
}
/**
* Drops the columns in the table.
*/
async dropColumns(tableSchema: TableSchema, columns: ColumnSchema[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const newTable = tableSchema.clone();
newTable.removeColumns(columns);
return this.recreateTable(newTable);
}
/**
* Creates a new foreign keys.
*/
async createForeignKeys(tableSchema: TableSchema, foreignKeys: ForeignKeySchema[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const newTable = tableSchema.clone();
newTable.addForeignKeys(foreignKeys);
return this.recreateTable(newTable);
}
/**
* Drops a foreign keys from the table.
*/
async dropForeignKeys(tableSchema: TableSchema, foreignKeys: ForeignKeySchema[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const newTable = tableSchema.clone();
newTable.removeForeignKeys(foreignKeys);
return this.recreateTable(newTable);
}
/**
* Creates a new index.
*/
async createIndex(tableName: string, index: IndexMetadata): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const sql = `CREATE ${index.isUnique ? "UNIQUE" : ""} INDEX "${index.name}" ON "${tableName}"("${index.columns.join("\", \"")}")`;
await this.query(sql);
}
/**
* Drops an index from the table.
*/
async dropIndex(tableName: string, indexName: string, isGenerated: boolean = false): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const sql = `DROP INDEX ${indexName}"`;
await this.query(sql);
}
/**
* Creates a new unique key.
*/
async createUniqueKey(tableName: string, columnName: string, keyName: string): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const sql = `CREATE UNIQUE INDEX "${keyName}" ON "${tableName}"("${columnName}")`;
await this.query(sql);
}
/**
* Creates a database type from a given column metadata.
*/
normalizeType(column: ColumnMetadata) {
switch (column.normalizedDataType) {
case "string":
return "character varying(" + (column.length ? column.length : 255) + ")";
case "text":
return "text";
case "boolean":
return "boolean";
case "integer":
case "int":
return "integer";
case "smallint":
return "smallint";
case "bigint":
return "bigint";
case "float":
return "real";
case "double":
case "number":
return "double precision";
case "decimal":
if (column.precision && column.scale) {
return `decimal(${column.precision},${column.scale})`;
} else if (column.scale) {
return `decimal(${column.scale})`;
} else if (column.precision) {
return `decimal(${column.precision})`;
} else {
return "decimal";
}
case "date":
return "date";
case "time":
if (column.timezone) {
return "time with time zone";
} else {
return "time without time zone";
}
case "datetime":
if (column.timezone) {
return "timestamp with time zone";
} else {
return "timestamp without time zone";
}
case "json":
return "json";
case "simple_array":
return column.length ? "character varying(" + column.length + ")" : "text";
}
throw new DataTypeNotSupportedByDriverError(column.type, "SQLite");
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
/**
* Database name shortcut.
*/
protected get dbName(): string {
return this.driver.options.database as string;
}
/**
* Parametrizes given object of values. Used to create column=value queries.
*/
protected parametrize(objectLiteral: ObjectLiteral, startIndex: number = 0): string[] {
return Object.keys(objectLiteral).map((key, index) => this.driver.escapeColumnName(key) + "=$" + (startIndex + index + 1));
}
/**
* Builds a query for create column.
*/
protected buildCreateColumnSql(column: ColumnSchema, skipPrimary: boolean, createForeignKeys?: boolean): string;
protected buildCreateColumnSql(column: ColumnMetadata, skipPrimary: boolean, createForeignKeys?: boolean): string;
protected buildCreateColumnSql(column: ColumnMetadata|ColumnSchema, skipPrimary: boolean, createForeignKeys: boolean = false): string {
let c = "\"" + column.name + "\"";
if (column instanceof ColumnMetadata) {
c += " " + this.normalizeType(column);
} else {
c += " " + column.type;
}
if (column.isNullable !== true)
c += " NOT NULL";
if (column.isPrimary === true && !skipPrimary) // todo: don't use primary keys this way at all
c += " PRIMARY KEY";
if (column.isGenerated === true) // don't use skipPrimary here since updates can update already exist primary without auto inc.
c += " AUTOINCREMENT";
// if (column instanceof ColumnMetadata && column.foreignKeys.length > 0 && createForeignKeys)
// c += ` REFERENCES "${column.foreignKeys[0].referencedTable.name}"("${column.foreignKeys[0].referencedColumnNames.join("\", \"")}")`; // todo: add multiple foreign keys support
// if (column instanceof ColumnSchema && column.foreignKeys.length > 0 && createForeignKeys)
// c += ` REFERENCES "${column.foreignKeys[0].toTable}"("${column.foreignKeys[0].to}")`; // todo: add multiple foreign keys support
return c;
}
protected async recreateTable(tableSchema: TableSchema,
options?: { createForeignKeys?: boolean }): Promise<void> {
// const withoutForeignKeyColumns = columns.filter(column => column.foreignKeys.length === 0);
// const createForeignKeys = options && options.createForeignKeys;
const columnDefinitions = tableSchema.columns.map(dbColumn => this.buildCreateColumnSql(dbColumn, false)).join(", ");
const columnNames = tableSchema.columns.map(column => `"${column.name}"`).join(", ");
let sql1 = `CREATE TABLE "temporary_${tableSchema.name}" (${columnDefinitions}`;
// if (options && options.createForeignKeys) {
tableSchema.foreignKeys.forEach(foreignKey => {
const columnNames = foreignKey.columnNames.map(name => `"${name}"`).join(", ");
const referencedColumnNames = foreignKey.referencedColumnNames.map(name => `"${name}"`).join(", ");
sql1 += `, FOREIGN KEY(${columnNames}) REFERENCES "${foreignKey.referencedTableName}"(${referencedColumnNames})`;
});
if (tableSchema.primaryKey) {
sql1 += `, PRIMARY KEY(${tableSchema.primaryKey.name})`;
}
sql1 += ")";
// todo: need also create uniques and indices?
// if (options && options.createIndices)
await this.query(sql1);
const sql2 = `INSERT INTO "temporary_${tableSchema.name}" SELECT ${columnNames} FROM "${tableSchema.name}"`;
await this.query(sql2);
const sql3 = `DROP TABLE "${tableSchema.name}"`;
await this.query(sql3);
const sql4 = `ALTER TABLE "temporary_${tableSchema.name}" RENAME TO "${tableSchema.name}"`;
await this.query(sql4);
}
}

View File

@ -40,6 +40,12 @@ export * from "./decorator/tables/Table";
export * from "./decorator/tables/AbstractTable";
export * from "./decorator/tree/TreeLevelColumn";
export * from "./decorator/tree/TreeParent";
export * from "./decorator/options/ColumnOptions";
export * from "./decorator/options/CompositeIndexOptions";
export * from "./decorator/options/JoinColumnOptions";
export * from "./decorator/options/JoinTableOptions";
export * from "./decorator/options/RelationOptions";
export * from "./decorator/options/TableOptions";
export {Connection} from "./connection/Connection";
export {DriverOptions} from "./driver/DriverOptions";

View File

@ -960,7 +960,7 @@ export class QueryBuilder<Entity> {
const parentAlias = join.alias.parentAliasName;
if (!parentAlias) {
return " " + joinType + " JOIN " + this.driver.escapeTableName(joinTableName) + " " + this.driver.escapeAliasName(join.alias.name) + " " + (join.condition ? ( join.conditionType + " " + join.condition ) : "");
return " " + joinType + " JOIN " + this.driver.escapeTableName(joinTableName) + " " + this.driver.escapeAliasName(join.alias.name) + " " + (join.condition ? ( join.conditionType + " " + this.replacePropertyNames(join.condition) ) : "");
}
const foundAlias = this.aliasMap.findAliasByName(parentAlias);

View File

@ -1,5 +1,6 @@
import {ColumnMetadata} from "../metadata/ColumnMetadata";
import {QueryRunner} from "../driver/QueryRunner";
import {ForeignKeySchema} from "./ForeignKeySchema";
export class ColumnSchema {
@ -7,9 +8,23 @@ export class ColumnSchema {
type: string;
default: string;
isNullable: boolean;
isGenerated: boolean;
isGenerated: boolean = false;
isPrimary: boolean;
comment: string|undefined;
foreignKeys: ForeignKeySchema[] = [];
constructor(existColumnSchema?: ColumnSchema) {
if (existColumnSchema) {
this.name = existColumnSchema.name;
this.type = existColumnSchema.type;
this.default = existColumnSchema.default;
this.isNullable = existColumnSchema.isNullable;
this.isGenerated = existColumnSchema.isGenerated;
this.isPrimary = existColumnSchema.isPrimary;
this.comment = existColumnSchema.comment;
this.foreignKeys = existColumnSchema.foreignKeys;
}
}
static create(queryRunner: QueryRunner, columnMetadata: ColumnMetadata) {
const columnSchema = new ColumnSchema();

View File

@ -1,9 +1,29 @@
import {ForeignKeyMetadata} from "../metadata/ForeignKeyMetadata";
export class ForeignKeySchema {
name: string;
columnNames: string[];
referencedColumnNames: string[];
referencedTableName: string;
onDelete?: string;
constructor(name: string) {
constructor(name: string, columnNames: string[], referencedColumnNames: string[], referencedTable: string, onDelete?: string) {
this.name = name;
this.columnNames = columnNames;
this.referencedColumnNames = referencedColumnNames;
this.referencedTableName = referencedTable;
this.onDelete = onDelete;
}
static createFromMetadata(metadata: ForeignKeyMetadata) {
return new ForeignKeySchema(
metadata.name,
metadata.columnNames,
metadata.referencedColumnNames,
metadata.referencedTableName,
metadata.onDelete
);
}
}

View File

@ -8,6 +8,7 @@ import {UniqueKeySchema} from "./UniqueKeySchema";
import {IndexSchema} from "./IndexSchema";
import {Driver} from "../driver/Driver";
import {QueryRunner} from "../driver/QueryRunner";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategyInterface";
/**
* Creates indexes based on the given metadata.
@ -38,7 +39,8 @@ export class SchemaBuilder {
// -------------------------------------------------------------------------
constructor(private driver: Driver,
private entityMetadatas: EntityMetadataCollection) {
private entityMetadatas: EntityMetadataCollection,
private namingStrategy: NamingStrategyInterface) {
}
// -------------------------------------------------------------------------
@ -53,7 +55,7 @@ export class SchemaBuilder {
const metadatas = this.entityMetadatas; // todo: save to this
this.queryRunner = await this.driver.createQueryRunner();
const tableSchemas = await this.loadSchemaTables(metadatas);
// console.log(tableSchemas);
// console.log("loaded table schemas: ", tableSchemas);
await this.queryRunner.beginTransaction();
try {
@ -62,6 +64,7 @@ export class SchemaBuilder {
await this.dropRemovedColumnsForAll(metadatas, tableSchemas);
await this.addNewColumnsForAll(metadatas, tableSchemas);
await this.updateExistColumnsForAll(metadatas, tableSchemas);
await this.createPrimaryKeysForAll(metadatas, tableSchemas);
await this.createForeignKeysForAll(metadatas, tableSchemas);
await this.updateUniqueKeysForAll(metadatas, tableSchemas);
await this.createIndicesForAll(metadatas, tableSchemas);
@ -85,46 +88,46 @@ export class SchemaBuilder {
*/
private loadSchemaTables(metadatas: EntityMetadata[]): Promise<TableSchema[]> {
const tableNames = metadatas.map(metadata => metadata.table.name);
return this.queryRunner.loadSchemaTables(tableNames);
return this.queryRunner.loadSchemaTables(tableNames, this.namingStrategy);
}
/**
* Drops all (old) foreign keys that exist in the table, but does not exist in the metadata.
*/
private async dropOldForeignKeysForAll(metadatas: EntityMetadata[], dbTables: TableSchema[]): Promise<void> {
let promises: Promise<any>[] = [];
metadatas.forEach(metadata => {
await Promise.all(metadatas.map(async metadata => {
const dbTable = dbTables.find(table => table.name === metadata.table.name);
if (!dbTable) return;
dbTable.foreignKeys
.filter(dbForeignKey => !metadata.foreignKeys.find(foreignKey => foreignKey.name === dbForeignKey.name))
.forEach(dbForeignKey => {
const promise = this.queryRunner
.dropForeignKey(metadata.table.name, dbForeignKey.name)
.then(() => dbTable.removeForeignKey(dbForeignKey));
promises.push(promise);
});
});
await Promise.all(promises);
const dbForeignKeysNeedToDrop = dbTable.foreignKeys.filter(dbForeignKey => {
return !metadata.foreignKeys.find(foreignKey => foreignKey.name === dbForeignKey.name);
});
if (dbForeignKeysNeedToDrop.length > 0) {
console.log(`dropping old foreign keys of ${dbTable.name}: ${dbForeignKeysNeedToDrop.map(dbForeignKey => dbForeignKey.name).join(", ")}`);
await this.queryRunner.dropForeignKeys(dbTable, dbForeignKeysNeedToDrop);
dbTable.removeForeignKeys(dbForeignKeysNeedToDrop);
}
}));
}
/**
* Creates a new table if it does not exist.
* New tables are created without keys.
*/
private async createNewTablesForAll(metadatas: EntityMetadata[], dbTables: TableSchema[]): Promise<void> {
await Promise.all(metadatas.map(async metadata => {
const dbTable = dbTables.find(table => table.name === metadata.table.name);
if (!dbTable) {
await this.queryRunner.createTable(metadata.table, metadata.columns);
dbTables.push(TableSchema.createFromMetadata(this.queryRunner, metadata.table, metadata.columns));
console.log(`creating a new table: ${metadata.table.name}`);
const createdColumns = await this.queryRunner.createTable(metadata.table, metadata.columns);
dbTables.push(TableSchema.createFromMetadata(this.queryRunner, metadata.table, createdColumns));
}
}));
}
/**
* Drops all columns exist (left old) in the table, but does not exist in the metadata.
* We drop their keys too, since it should be safe.
*/
private dropRemovedColumnsForAll(metadatas: EntityMetadata[], dbTables: TableSchema[]) {
const allForeignKeys = metadatas.reduce((all, metadata) => all.concat(metadata.foreignKeys), [] as ForeignKeyMetadata[]);
@ -132,20 +135,24 @@ export class SchemaBuilder {
return Promise.all(metadatas.map(async metadata => {
const dbTable = dbTables.find(table => table.name === metadata.table.name);
if (!dbTable) return;
const dropColumnQueries = dbTable.columns
.filter(dbColumn => !metadata.columns.find(column => column.name === dbColumn.name))
.map(async dbColumn => {
await this.dropAllColumnRelatedForeignKeys(metadata.table.name, dbColumn.name, allForeignKeys, dbTables);
await this.queryRunner.dropColumn(metadata.table.name, dbColumn.name);
dbTable.removeColumn(dbColumn);
});
return Promise.all(dropColumnQueries);
const droppedColumns = dbTable.columns.filter(dbColumn => !metadata.columns.find(column => column.name === dbColumn.name));
if (droppedColumns.length > 0) {
console.log(`columns dropped in ${dbTable.name}: `, droppedColumns.map(column => column.name).join(", "));
const dropRelatedForeignKeysPromises = droppedColumns.map(async droppedColumn => {
return this.dropAllColumnRelatedForeignKeys(metadata.table.name, droppedColumn.name, allForeignKeys, dbTables);
});
await Promise.all(dropRelatedForeignKeysPromises);
await this.queryRunner.dropColumns(dbTable, droppedColumns);
dbTable.removeColumns(droppedColumns);
}
}));
}
/**
* Adds columns from metadata which does not exist in the table.
* Columns are created without keys.
*/
private addNewColumnsForAll(metadatas: EntityMetadata[], dbTables: TableSchema[]) {
// return Promise.all(metadatas.map(metadata => this.addNewColumns(metadata.table, metadata.columns)));
@ -153,39 +160,75 @@ export class SchemaBuilder {
const dbTable = dbTables.find(table => table.name === metadata.table.name);
if (!dbTable) return;
const newColumnQueries = metadata.columns
.filter(column => !dbTable.columns.find(dbColumn => dbColumn.name === column.name))
.map(async column => {
await this.queryRunner.createColumn(metadata.table.name, column);
dbTable.columns.push(ColumnSchema.create(this.queryRunner, column));
const newColumns = metadata.columns.filter(column => !dbTable.columns.find(dbColumn => dbColumn.name === column.name));
if (newColumns.length > 0) {
console.log(`new columns added: `, newColumns.map(column => column.name).join(", "));
const createdColumns = await this.queryRunner.createColumns(dbTable, newColumns);
createdColumns.forEach(createdColumn => {
dbTable.columns.push(ColumnSchema.create(this.queryRunner, createdColumn));
});
}
return Promise.all(newColumnQueries);
}));
}
/**
* Update all exist columns which metadata has changed.
* Still don't create keys. Also we don't touch foreign keys of the changed columns.
*/
private updateExistColumnsForAll(metadatas: EntityMetadata[], dbTables: TableSchema[]) {
const allForeignKeys = metadatas.reduce((all, metadata) => all.concat(metadata.foreignKeys), [] as ForeignKeyMetadata[]);
// return Promise.all(metadatas.map(metadata => this.updateExistColumns(metadata.table, metadata.columns, allKeys)));
return Promise.all(metadatas.map(async metadata => {
const dbTable = dbTables.find(table => table.name === metadata.table.name);
if (!dbTable) return;
const updateQueries = dbTable
.findChangedColumns(this.queryRunner, metadata.columns)
.map(async changedColumn => {
const updateColumns = dbTable.findChangedColumns(this.queryRunner, metadata.columns);
if (updateColumns.length > 0) {
console.log(`columns changed in ${dbTable.name}. updating: `, updateColumns.map(column => column.name).join(", "));
// drop all foreign keys that point to this column
const dropRelatedForeignKeysPromises = updateColumns
.filter(changedColumn => !!metadata.columns.find(column => column.name === changedColumn.name))
.map(changedColumn => this.dropAllColumnRelatedForeignKeys(metadata.table.name, changedColumn.name, allForeignKeys, dbTables));
// wait until all related foreign keys are dropped
await Promise.all(dropRelatedForeignKeysPromises);
// generate a map of new/old columns
const newAndOldColumns = updateColumns.map(changedColumn => {
const column = metadata.columns.find(column => column.name === changedColumn.name);
if (!column)
throw new Error(`Column ${changedColumn.name} was not found in the given columns`);
await this.dropAllColumnRelatedForeignKeys(metadata.table.name, column.name, allForeignKeys, dbTables);
await this.queryRunner.changeColumn(metadata.table.name, changedColumn, column);
return {
newColumn: column,
oldColumn: changedColumn
};
});
return Promise.all(updateQueries);
return this.queryRunner.changeColumns(dbTable, newAndOldColumns);
}
}));
}
/**
* Creates primary keys which does not exist in the table yet.
*/
private createPrimaryKeysForAll(metadatas: EntityMetadata[], dbTables: TableSchema[]) {
return Promise.all(metadatas.map(async metadata => {
const dbTable = dbTables.find(table => table.name === metadata.table.name);
if (!dbTable)
return;
// const newKeys = metadata.primaryKeys.filter(primaryKey => {
// return !dbTable.primaryKeys.find(dbPrimaryKey => dbPrimaryKey.name === primaryKey.name)
// });
// if (newKeys.length > 0) {
// console.log(dbTable.foreignKeys);
// console.log(`creating a primary keys: ${newKeys.map(key => key.name).join(", ")}`);
// await this.queryRunner.createPrimaryKeys(dbTable, newKeys);
// dbTable.addPrimaryKeys(newKeys);
// }
}));
}
@ -197,14 +240,15 @@ export class SchemaBuilder {
const dbTable = dbTables.find(table => table.name === metadata.table.name);
if (!dbTable) return;
const dropKeysQueries = metadata.foreignKeys
.filter(foreignKey => !dbTable.foreignKeys.find(dbForeignKey => dbForeignKey.name === foreignKey.name))
.map(async foreignKey => {
await this.queryRunner.createForeignKey(foreignKey);
dbTable.foreignKeys.push(new ForeignKeySchema(foreignKey.name));
});
return Promise.all(dropKeysQueries);
const newKeys = metadata.foreignKeys.filter(foreignKey => {
return !dbTable.foreignKeys.find(dbForeignKey => dbForeignKey.name === foreignKey.name);
});
if (newKeys.length > 0) {
const dbForeignKeys = newKeys.map(foreignKeyMetadata => ForeignKeySchema.createFromMetadata(foreignKeyMetadata));
console.log(`creating a foreign keys: ${newKeys.map(key => key.name).join(", ")}`);
await this.queryRunner.createForeignKeys(dbTable, dbForeignKeys);
dbTable.addForeignKeys(dbForeignKeys);
}
}));
}
@ -310,13 +354,15 @@ export class SchemaBuilder {
if (!dependForeignKeys.length)
return;
await Promise.all(dependForeignKeys.map(async fk => {
const foreignKey = dbTable.foreignKeys.find(dbForeignKey => dbForeignKey.name === fk.name);
if (foreignKey) {
await this.queryRunner.dropForeignKey(fk.tableName, fk.name);
dbTable.removeForeignKey(foreignKey);
}
}));
const dependForeignKeyInTable = dependForeignKeys.filter(fk => {
return !!dbTable.foreignKeys.find(dbForeignKey => dbForeignKey.name === fk.name);
});
if (dependForeignKeyInTable.length > 0) {
console.log(`dropping related foreign keys of ${tableName}: ${dependForeignKeyInTable.map(foreignKey => foreignKey.name).join(", ")}`);
const dbForeignKeys = dependForeignKeyInTable.map(foreignKeyMetadata => ForeignKeySchema.createFromMetadata(foreignKeyMetadata));
await this.queryRunner.dropForeignKeys(dbTable, dbForeignKeys);
dbTable.removeForeignKeys(dbForeignKeys);
}
}
}

View File

@ -6,6 +6,7 @@ import {PrimaryKeySchema} from "./PrimaryKeySchema";
import {TableMetadata} from "../metadata/TableMetadata";
import {ColumnMetadata} from "../metadata/ColumnMetadata";
import {QueryRunner} from "../driver/QueryRunner";
import {ForeignKeyMetadata} from "../metadata/ForeignKeyMetadata";
export class TableSchema {
@ -16,14 +17,44 @@ export class TableSchema {
uniqueKeys: UniqueKeySchema[] = [];
primaryKey: PrimaryKeySchema|undefined;
/**
* Indicates if column has a generated (serial/auto-increment).
* This is needed because, for example it sqlite its not easy to determine
* which of your column has a generated flag.
*/
// hasGenerated: boolean;
constructor(name: string) {
this.name = name;
}
removeColumn(column: ColumnSchema) {
const index = this.columns.indexOf(column);
if (index !== -1)
this.columns.splice(index, 1);
clone(): TableSchema {
const cloned = new TableSchema(this.name);
cloned.columns = this.columns.map(column => new ColumnSchema(column));
cloned.indices = this.indices.map(index => new IndexSchema(index.name, index.columnNames));
cloned.foreignKeys = this.foreignKeys.map(key => new ForeignKeySchema(key.name, key.columnNames, key.referencedColumnNames, key.referencedTableName, key.onDelete));
cloned.uniqueKeys = this.uniqueKeys.map(key => new UniqueKeySchema(key.name));
if (this.primaryKey)
cloned.primaryKey = new PrimaryKeySchema(this.primaryKey.name);
return cloned;
}
addForeignKeys(foreignKeys: ForeignKeySchema[]) {
this.foreignKeys = this.foreignKeys.concat(foreignKeys);
}
addColumns(columns: ColumnSchema[]) {
this.columns = this.columns.concat(columns);
}
removeColumns(columns: ColumnSchema[]) {
columns.forEach(column => this.removeColumn(column));
}
removeColumn(columnToRemove: ColumnSchema) {
const foundColumn = this.columns.find(column => column.name === columnToRemove.name);
if (foundColumn)
this.columns.splice(this.columns.indexOf(foundColumn), 1);
}
removeIndex(indexSchema: IndexSchema) {
@ -38,6 +69,12 @@ export class TableSchema {
this.foreignKeys.splice(index, 1);
}
removeForeignKeys(dbForeignKeys: ForeignKeySchema[]) {
dbForeignKeys.forEach(foreignKey => {
this.removeForeignKey(foreignKey);
});
}
removeUniqueByName(name: string) {
const uniqueKey = this.uniqueKeys.find(uniqueKey => uniqueKey.name === name);
if (!uniqueKey)
@ -66,13 +103,20 @@ export class TableSchema {
return this.columns.filter(columnSchema => {
const columnMetadata = columnMetadatas.find(columnMetadata => columnMetadata.name === columnSchema.name);
if (!columnMetadata) return false; // we don't need new columns, we only need exist and changed
// const bothGenerated = (this.hasGenerated && columnMetadata.isGenerated) || (columnSchema.isGenerated === columnMetadata.isGenerated);
// console.log("--------");
// console.log(this.name);
// console.log("this.hasGenerated: ", this.hasGenerated);
// console.log(columnSchema.name, ": ", !bothGenerated);
return columnSchema.name !== columnMetadata.name ||
columnSchema.type !== queryRunner.normalizeType(columnMetadata) ||
columnSchema.comment !== columnMetadata.comment ||
columnSchema.default !== columnMetadata.default ||
columnSchema.isNullable !== columnMetadata.isNullable ||
// columnSchema.isPrimary !== columnMetadata.isPrimary ||
columnSchema.isGenerated !== columnMetadata.isGenerated;
// columnSchema.isPrimary !== columnMetadata.isPrimary ||
// !bothGenerated;
});
}
}

2
temp/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
**
!.gitignore

View File

@ -10,9 +10,9 @@ describe("lazy-relations", () => {
const profileSchema = require(resourceDir + "schema/profile.json");
let connections: Connection[];
beforeEach(() => setupTestingConnections({ entities: [Post, Category], entitySchemas: [userSchema, profileSchema], schemaCreate: true }).then(all => connections = all));
before(() => setupTestingConnections({ entities: [Post, Category], entitySchemas: [userSchema, profileSchema], schemaCreate: true }).then(all => connections = all));
beforeEach(() => reloadDatabases(connections));
afterEach(() => closeConnections(connections));
after(() => closeConnections(connections));
it("should persist and hydrate successfully on a relation without inverse side", () => Promise.all(connections.map(async connection => {
const postRepository = connection.getRepository(Post);

View File

@ -17,7 +17,7 @@ describe("persistence > many-to-many", function() {
const parameters: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -16,7 +16,7 @@ describe("repository > removeById and removeByIds methods", function() {
const parameters: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -17,7 +17,7 @@ describe("repository > set/add/remove relation methods", function() {
const parameters: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",

View File

@ -14,23 +14,26 @@ export interface TestingConnectionOptions {
skipMysql?: boolean;
skipMariadb?: boolean;
skipPostgres?: boolean;
skipSqlite?: boolean;
}
export function closeConnections(connections: Connection[]) {
return Promise.all(connections.map(connection => connection.isConnected ? connection.close() : undefined));
}
export function createTestingConnectionOptions(type: "mysql"|"mysqlSecondary"|"mariadb"|"mariadbSecondary"|"postgres"|"postgresSecondary"): DriverOptions {
export function createTestingConnectionOptions(type: "mysql"|"mysqlSecondary"|"mariadb"|"mariadbSecondary"|"postgres"|"postgresSecondary"|"sqlite"|"sqliteSecondary"): DriverOptions {
const parameters = require(__dirname + "/../../../../config/parameters.json"); // path is relative to compile directory
// const parameters = require(__dirname + "/../../config/parameters.json");
let driverType: "mysql"|"mariadb"|"postgres" = "mysql"; // = type === "mysql" || type === "mysqlSecondary" ? "mysql" : "postgres";
let driverType: "mysql"|"mariadb"|"postgres"|"sqlite" = "mysql"; // = type === "mysql" || type === "mysqlSecondary" ? "mysql" : "postgres";
if (type === "mysql" || type === "mysqlSecondary") {
driverType = "mysql";
} else if (type === "mariadb" || type === "mariadbSecondary") {
driverType = "mariadb";
} else if (type === "postgres" || type === "postgresSecondary") {
driverType = "postgres";
} else if (type === "sqlite" || type === "sqliteSecondary") {
driverType = "sqlite";
}
return {
@ -40,6 +43,7 @@ export function createTestingConnectionOptions(type: "mysql"|"mysqlSecondary"|"m
username: parameters.connections[type].username,
password: parameters.connections[type].password,
database: parameters.connections[type].database,
storage: parameters.connections[type].storage,
extra: {
max: 500
}
@ -132,9 +136,38 @@ export async function setupTestingConnections(options?: TestingConnectionOptions
},
};
const sqliteParameters: ConnectionOptions = {
name: "sqlitePrimaryConnection",
driver: createTestingConnectionOptions("sqlite"),
autoSchemaCreate: options && options.entities ? options.schemaCreate : false,
entities: options && options.entities ? options.entities : [],
entitySchemas: options && options.entitySchemas ? options.entitySchemas : [],
entityDirectories: options && options.entityDirectories ? options.entityDirectories : [],
logging: {
// logQueries: true, // uncomment for debugging
logOnlyFailedQueries: true,
logFailedQueryError: true
},
};
const sqliteSecondaryParameters: ConnectionOptions = {
name: "sqliteSecondaryConnection",
driver: createTestingConnectionOptions("sqliteSecondary"),
autoSchemaCreate: options && options.entities ? options.schemaCreate : false,
entities: options && options.entities ? options.entities : [],
entitySchemas: options && options.entitySchemas ? options.entitySchemas : [],
entityDirectories: options && options.entityDirectories ? options.entityDirectories : [],
logging: {
// logQueries: true, // uncomment for debugging
logOnlyFailedQueries: true,
logFailedQueryError: true
},
};
const mysql = !options || !options.skipMysql;
const mariadb = !options || !options.skipMariadb;
const postgres = !options || !options.skipPostgres;
const sqlite = !options || !options.skipSqlite;
const allParameters: ConnectionOptions[] = [];
if (mysql)
@ -143,12 +176,16 @@ export async function setupTestingConnections(options?: TestingConnectionOptions
allParameters.push(mariadbParameters);
if (postgres)
allParameters.push(postgresParameters);
if (sqlite)
allParameters.push(sqliteParameters);
if (mysql && options && options.secondaryConnections)
allParameters.push(mysqlSecondaryParameters);
if (mariadb && options && options.secondaryConnections)
allParameters.push(mariadbSecondaryParameters);
if (postgres && options && options.secondaryConnections)
allParameters.push(postgresSecondaryParameters);
if (sqlite && options && options.secondaryConnections)
allParameters.push(sqliteSecondaryParameters);
return Promise.all(allParameters.map(async parameters => {
const connection = await createConnection(parameters);
@ -167,7 +204,7 @@ export function setupConnection(callback: (connection: Connection) => any, entit
const parameters: ConnectionOptions = {
driver: {
type: "mysql",
host: "192.168.99.100",
host: "localhost",
port: 3306,
username: "root",
password: "admin",