mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
added basic sqlite driver
This commit is contained in:
parent
7fc08acecb
commit
333c212791
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,6 @@
|
||||
build/
|
||||
coverage/
|
||||
node_modules/
|
||||
typings/
|
||||
coverage/
|
||||
npm-debug.log
|
||||
config/parameters.json
|
||||
@ -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",
|
||||
|
||||
@ -14,6 +14,12 @@
|
||||
"password": "admin",
|
||||
"database": "test2"
|
||||
},
|
||||
"sqlite": {
|
||||
"storage": "temp/sqlitedb.db"
|
||||
},
|
||||
"sqliteSecondary": {
|
||||
"storage": "temp/sqlitedb-secondary.db"
|
||||
},
|
||||
"postgres": {
|
||||
"host": "localhost",
|
||||
"port": 5432,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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/*"]
|
||||
};
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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) + ")";
|
||||
|
||||
271
src/driver/sqlite/SqliteDriver.ts
Normal file
271
src/driver/sqlite/SqliteDriver.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
591
src/driver/sqlite/SqliteQueryRunner.ts
Normal file
591
src/driver/sqlite/SqliteQueryRunner.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
2
temp/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
**
|
||||
!.gitignore
|
||||
@ -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);
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user