mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
feat: sqlite attach (#8396)
* Updated to latest master. Cleans up codestyle, ignores yarn * re-adds the portable path tests * mapping bug fix that appears with unrecognised existing tables * Review comments * review comments * test breakage with node 12
This commit is contained in:
parent
31f0b5535a
commit
9e844d9ff7
4
.gitignore
vendored
4
.gitignore
vendored
@ -9,3 +9,7 @@ node_modules/
|
||||
ormlogs.log
|
||||
npm-debug.log
|
||||
/test/github-issues/799/tmp/*
|
||||
# ignore yarn2 artifacts but allow yarn.lock (forces yarn1 compat which is node_modules)
|
||||
.yarn/
|
||||
.yarn*
|
||||
|
||||
|
||||
34
package.json
34
package.json
@ -21,25 +21,25 @@
|
||||
"types": "./index.d.ts",
|
||||
"type": "commonjs",
|
||||
"browser": {
|
||||
"./browser/driver/aurora-data-api/AuroraDataApiDriver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/cockroachdb/CockroachDriver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/postgres/PostgresDriver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/oracle/OracleDriver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/sap/SapDriver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/mysql/MysqlDriver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/sqlserver/SqlServerDriver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/mongodb/MongoDriver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/mongodb/MongoQueryRunner.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/entity-manager/MongoEntityManager.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/repository/MongoRepository.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/sqlite/SqliteDriver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/better-sqlite3/BetterSqlite3Driver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/util/DirectoryExportedClassesLoader.js": "./browser/platform/BrowserDirectoryExportedClassesLoader.js",
|
||||
"./browser/logger/FileLogger.js": "./browser/platform/BrowserFileLoggerDummy.js",
|
||||
"./browser/connection/ConnectionOptionsReader.js": "./browser/platform/BrowserConnectionOptionsReaderDummy.js",
|
||||
"./browser/connection/options-reader/ConnectionOptionsXmlReader.js": "./browser/platform/BrowserConnectionOptionsReaderDummy.js",
|
||||
"./browser/connection/options-reader/ConnectionOptionsYmlReader.js": "./browser/platform/BrowserConnectionOptionsReaderDummy.js",
|
||||
"./browser/driver/aurora-data-api/AuroraDataApiDriver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/better-sqlite3/BetterSqlite3Driver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/cockroachdb/CockroachDriver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/mongodb/MongoDriver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/mongodb/MongoQueryRunner.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/mysql/MysqlDriver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/oracle/OracleDriver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/postgres/PostgresDriver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/sap/SapDriver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/sqlite/SqliteDriver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/driver/sqlserver/SqlServerDriver.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/entity-manager/MongoEntityManager.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/logger/FileLogger.js": "./browser/platform/BrowserFileLoggerDummy.js",
|
||||
"./browser/platform/PlatformTools.js": "./browser/platform/BrowserPlatformTools.js",
|
||||
"./browser/repository/MongoRepository.js": "./browser/platform/BrowserDisabledDriversDummy.js",
|
||||
"./browser/util/DirectoryExportedClassesLoader.js": "./browser/platform/BrowserDirectoryExportedClassesLoader.js",
|
||||
"./index.js": "./browser/index.js"
|
||||
},
|
||||
"repository": {
|
||||
@ -223,9 +223,7 @@
|
||||
"lint": "eslint -c ./.eslintrc.js src/**/*.ts test/**/*.ts sample/**/*.ts",
|
||||
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 2"
|
||||
},
|
||||
"bin": {
|
||||
"typeorm": "./cli.js"
|
||||
},
|
||||
"bin": "./cli.js",
|
||||
"funding": "https://opencollective.com/typeorm",
|
||||
"collective": {
|
||||
"type": "opencollective",
|
||||
|
||||
@ -121,6 +121,12 @@ export interface BaseConnectionOptions {
|
||||
*/
|
||||
readonly extra?: any;
|
||||
|
||||
/**
|
||||
* Holds reference to the baseDirectory where configuration file are expected
|
||||
* @internal
|
||||
*/
|
||||
baseDirectory?: string;
|
||||
|
||||
/**
|
||||
* Allows to setup cache options.
|
||||
*/
|
||||
|
||||
@ -33,6 +33,7 @@ import {SqljsEntityManager} from "../entity-manager/SqljsEntityManager";
|
||||
import {RelationLoader} from "../query-builder/RelationLoader";
|
||||
import {EntitySchema} from "../entity-schema/EntitySchema";
|
||||
import {SqlServerDriver} from "../driver/sqlserver/SqlServerDriver";
|
||||
import {AbstractSqliteDriver} from "../driver/sqlite-abstract/AbstractSqliteDriver";
|
||||
import {MysqlDriver} from "../driver/mysql/MysqlDriver";
|
||||
import {ObjectUtils} from "../util/ObjectUtils";
|
||||
import {IsolationLevel} from "../driver/types/IsolationLevel";
|
||||
@ -260,15 +261,23 @@ export class Connection {
|
||||
async dropDatabase(): Promise<void> {
|
||||
const queryRunner = this.createQueryRunner();
|
||||
try {
|
||||
if (this.driver instanceof SqlServerDriver || this.driver instanceof MysqlDriver || this.driver instanceof AuroraDataApiDriver) {
|
||||
const databases: string[] = this.driver.database ? [this.driver.database] : [];
|
||||
if (this.driver instanceof SqlServerDriver || this.driver instanceof MysqlDriver || this.driver instanceof AuroraDataApiDriver || this.driver instanceof AbstractSqliteDriver) {
|
||||
const databases: string[] = [];
|
||||
this.entityMetadatas.forEach(metadata => {
|
||||
if (metadata.database && databases.indexOf(metadata.database) === -1)
|
||||
databases.push(metadata.database);
|
||||
});
|
||||
if (databases.length === 0 && this.driver.database) {
|
||||
databases.push(this.driver.database);
|
||||
};
|
||||
|
||||
for (const database of databases) {
|
||||
await queryRunner.clearDatabase(database);
|
||||
if (databases.length === 0) {
|
||||
await queryRunner.clearDatabase();
|
||||
}
|
||||
else {
|
||||
for (const database of databases) {
|
||||
await queryRunner.clearDatabase(database);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await queryRunner.clearDatabase();
|
||||
|
||||
@ -6,6 +6,7 @@ import {ConnectionOptionsEnvReader} from "./options-reader/ConnectionOptionsEnvR
|
||||
import {ConnectionOptionsYmlReader} from "./options-reader/ConnectionOptionsYmlReader";
|
||||
import {ConnectionOptionsXmlReader} from "./options-reader/ConnectionOptionsXmlReader";
|
||||
import { TypeORMError } from "../error";
|
||||
import { isAbsolute } from "../util/PathUtils";
|
||||
import {importOrRequireFile} from "../util/ImportUtils";
|
||||
|
||||
/**
|
||||
@ -151,6 +152,7 @@ export class ConnectionOptionsReader {
|
||||
connectionOptions = [connectionOptions];
|
||||
|
||||
connectionOptions.forEach(options => {
|
||||
options.baseDirectory = this.baseDirectory;
|
||||
if (options.entities) {
|
||||
const entities = (options.entities as any[]).map(entity => {
|
||||
if (typeof entity === "string" && entity.substr(0, 1) !== "/")
|
||||
@ -181,7 +183,7 @@ export class ConnectionOptionsReader {
|
||||
|
||||
// make database path file in sqlite relative to package.json
|
||||
if (options.type === "sqlite" || options.type === "better-sqlite3") {
|
||||
if (typeof options.database === "string" &&
|
||||
if (typeof options.database === "string" && !isAbsolute(options.database) &&
|
||||
options.database.substr(0, 1) !== "/" && // unix absolute
|
||||
options.database.substr(1, 2) !== ":\\" && // windows absolute
|
||||
options.database !== ":memory:") {
|
||||
|
||||
@ -10,6 +10,7 @@ import { AbstractSqliteDriver } from "../sqlite-abstract/AbstractSqliteDriver";
|
||||
import { BetterSqlite3ConnectionOptions } from "./BetterSqlite3ConnectionOptions";
|
||||
import { BetterSqlite3QueryRunner } from "./BetterSqlite3QueryRunner";
|
||||
import {ReplicationMode} from "../types/ReplicationMode";
|
||||
import { filepathToName, isAbsolute } from "../../util/PathUtils";
|
||||
|
||||
/**
|
||||
* Organizes communication with sqlite DBMS.
|
||||
@ -79,6 +80,34 @@ export class BetterSqlite3Driver extends AbstractSqliteDriver {
|
||||
return super.normalizeType(column);
|
||||
}
|
||||
|
||||
async afterConnect(): Promise<void> {
|
||||
return this.attachDatabases();
|
||||
}
|
||||
|
||||
/**
|
||||
* For SQLite, the database may be added in the decorator metadata. It will be a filepath to a database file.
|
||||
*/
|
||||
buildTableName(tableName: string, _schema?: string, database?: string): string {
|
||||
|
||||
if (!database) return tableName;
|
||||
if (this.getAttachedDatabaseHandleByRelativePath(database)) return `${this.getAttachedDatabaseHandleByRelativePath(database)}.${tableName}`;
|
||||
|
||||
if (database === this.options.database) return tableName;
|
||||
|
||||
// we use the decorated name as supplied when deriving attach handle (ideally without non-portable absolute path)
|
||||
const identifierHash = filepathToName(database);
|
||||
// decorated name will be assumed relative to main database file when non absolute. Paths supplied as absolute won't be portable
|
||||
const absFilepath = isAbsolute(database) ? database : path.join(this.getMainDatabasePath(), database);
|
||||
|
||||
this.attachedDatabases[database] = {
|
||||
attachFilepathAbsolute: absFilepath,
|
||||
attachFilepathRelative: database,
|
||||
attachHandle: identifierHash,
|
||||
};
|
||||
|
||||
return `${identifierHash}.${tableName}`;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Protected Methods
|
||||
// -------------------------------------------------------------------------
|
||||
@ -89,7 +118,7 @@ export class BetterSqlite3Driver extends AbstractSqliteDriver {
|
||||
protected async createDatabaseConnection() {
|
||||
// not to create database directory if is in memory
|
||||
if (this.options.database !== ":memory:")
|
||||
await this.createDatabaseDirectory(this.options.database);
|
||||
await this.createDatabaseDirectory(path.dirname(this.options.database));
|
||||
|
||||
const {
|
||||
database,
|
||||
@ -137,8 +166,28 @@ export class BetterSqlite3Driver extends AbstractSqliteDriver {
|
||||
/**
|
||||
* Auto creates database directory if it does not exist.
|
||||
*/
|
||||
protected async createDatabaseDirectory(fullPath: string): Promise<void> {
|
||||
await mkdirp(path.dirname(fullPath));
|
||||
protected async createDatabaseDirectory(dbPath: string): Promise<void> {
|
||||
await mkdirp(dbPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the attaching of the database files. The attachedDatabase should have been populated during calls to #buildTableName
|
||||
* during EntityMetadata production (see EntityMetadata#buildTablePath)
|
||||
*
|
||||
* https://sqlite.org/lang_attach.html
|
||||
*/
|
||||
protected async attachDatabases() {
|
||||
|
||||
// @todo - possibly check number of databases (but unqueriable at runtime sadly) - https://www.sqlite.org/limits.html#max_attached
|
||||
for await (const {attachHandle, attachFilepathAbsolute} of Object.values(this.attachedDatabases)) {
|
||||
await this.createDatabaseDirectory(path.dirname(attachFilepathAbsolute));
|
||||
await this.connection.query(`ATTACH "${attachFilepathAbsolute}" AS "${attachHandle}"`);
|
||||
}
|
||||
}
|
||||
|
||||
protected getMainDatabasePath(): string {
|
||||
const optionsDb = this.options.database;
|
||||
return path.dirname(isAbsolute(optionsDb) ? optionsDb : path.join(this.options.baseDirectory!, optionsDb));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -128,4 +128,19 @@ export class BetterSqlite3QueryRunner extends AbstractSqliteQueryRunner {
|
||||
throw new QueryFailedError(query, parameters, err);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Protected Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
protected async loadTableRecords(tablePath: string, tableOrIndex: "table" | "index") {
|
||||
const [database, tableName] = this.splitTablePath(tablePath);
|
||||
const res = await this.query(`SELECT ${database ? `'${database}'` : null} as database, * FROM ${this.escapePath(`${database ? `${database}.` : ""}sqlite_master`)} WHERE "type" = '${tableOrIndex}' AND "${tableOrIndex === "table" ? "name" : "tbl_name"}" IN ('${tableName}')`);
|
||||
return res;
|
||||
}
|
||||
protected async loadPragmaRecords(tablePath: string, pragma: string) {
|
||||
const [database, tableName] = this.splitTablePath(tablePath);
|
||||
const res = await this.query(`PRAGMA ${database ? `"${database}".` : ""}${pragma}("${tableName}")`);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,13 @@ import { Table } from "../../schema-builder/table/Table";
|
||||
import { View } from "../../schema-builder/view/View";
|
||||
import { TableForeignKey } from "../../schema-builder/table/TableForeignKey";
|
||||
|
||||
|
||||
type DatabasesMap = Record<string, {
|
||||
attachFilepathAbsolute: string
|
||||
attachFilepathRelative: string
|
||||
attachHandle: string
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Organizes communication with sqlite DBMS.
|
||||
*/
|
||||
@ -209,6 +216,15 @@ export abstract class AbstractSqliteDriver implements Driver {
|
||||
*/
|
||||
maxAliasLength?: number;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Protected Properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Any attached databases (excepting default 'main')
|
||||
*/
|
||||
attachedDatabases: DatabasesMap = {};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
@ -257,6 +273,18 @@ export abstract class AbstractSqliteDriver implements Driver {
|
||||
});
|
||||
}
|
||||
|
||||
hasAttachedDatabases(): boolean {
|
||||
return !!Object.keys(this.attachedDatabases).length;
|
||||
}
|
||||
|
||||
getAttachedDatabaseHandleByRelativePath(path: string): string | undefined {
|
||||
return this.attachedDatabases?.[path]?.attachHandle
|
||||
}
|
||||
|
||||
getAttachedDatabasePathRelativeByHandle(handle: string): string | undefined {
|
||||
return Object.values(this.attachedDatabases).find(({attachHandle}) => handle === attachHandle)?.attachFilepathRelative
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a schema builder used to build and sync a schema.
|
||||
*/
|
||||
@ -429,7 +457,7 @@ export abstract class AbstractSqliteDriver implements Driver {
|
||||
const driverSchema = undefined
|
||||
|
||||
if (target instanceof Table || target instanceof View) {
|
||||
const parsed = this.parseTableName(target.name);
|
||||
const parsed = this.parseTableName(target.schema ? `"${target.schema}"."${target.name}"` : target.name);
|
||||
|
||||
return {
|
||||
database: target.database || parsed.database || driverDatabase,
|
||||
@ -468,8 +496,9 @@ export abstract class AbstractSqliteDriver implements Driver {
|
||||
tableName: parts[2]
|
||||
};
|
||||
} else if (parts.length === 2) {
|
||||
const database = this.getAttachedDatabasePathRelativeByHandle(parts[0]) ?? driverDatabase
|
||||
return {
|
||||
database: driverDatabase,
|
||||
database: database,
|
||||
schema: parts[0],
|
||||
tableName: parts[1]
|
||||
};
|
||||
|
||||
@ -190,7 +190,7 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen
|
||||
*/
|
||||
async hasColumn(tableOrName: Table|string, columnName: string): Promise<boolean> {
|
||||
const tableName = tableOrName instanceof Table ? tableOrName.name : tableOrName;
|
||||
const sql = `PRAGMA table_info("${tableName}")`;
|
||||
const sql = `PRAGMA table_info(${this.escapePath(tableName)})`;
|
||||
const columns: ObjectLiteral[] = await this.query(sql);
|
||||
return !!columns.find(column => column["name"] === columnName);
|
||||
}
|
||||
@ -319,8 +319,8 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen
|
||||
newTable.name = newTableName;
|
||||
|
||||
// rename table
|
||||
const up = new Query(`ALTER TABLE "${oldTable.name}" RENAME TO "${newTableName}"`);
|
||||
const down = new Query(`ALTER TABLE "${newTableName}" RENAME TO "${oldTable.name}"`);
|
||||
const up = new Query(`ALTER TABLE ${this.escapePath(oldTable.name)} RENAME TO ${this.escapePath(newTableName)}`);
|
||||
const down = new Query(`ALTER TABLE ${this.escapePath(newTableName)} RENAME TO ${this.escapePath(oldTable.name)}`);
|
||||
await this.executeQueries(up, down);
|
||||
|
||||
// rename old table;
|
||||
@ -721,21 +721,27 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen
|
||||
* Note: this operation uses SQL's TRUNCATE query which cannot be reverted in transactions.
|
||||
*/
|
||||
async clearTable(tableName: string): Promise<void> {
|
||||
await this.query(`DELETE FROM "${tableName}"`);
|
||||
await this.query(`DELETE FROM ${this.escapePath(tableName)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all tables from the currently connected database.
|
||||
*/
|
||||
async clearDatabase(): Promise<void> {
|
||||
async clearDatabase(database?: string): Promise<void> {
|
||||
|
||||
let dbPath: string | undefined = undefined;
|
||||
if (database && this.driver.getAttachedDatabaseHandleByRelativePath(database)) {
|
||||
dbPath = this.driver.getAttachedDatabaseHandleByRelativePath(database);
|
||||
}
|
||||
|
||||
await this.query(`PRAGMA foreign_keys = OFF;`);
|
||||
await this.startTransaction();
|
||||
try {
|
||||
const selectViewDropsQuery = `SELECT 'DROP VIEW "' || name || '";' as query FROM "sqlite_master" WHERE "type" = 'view'`;
|
||||
const selectViewDropsQuery = dbPath ? `SELECT 'DROP VIEW "${dbPath}"."' || name || '";' as query FROM "${dbPath}"."sqlite_master" WHERE "type" = 'view'` : `SELECT 'DROP VIEW "' || name || '";' as query FROM "sqlite_master" WHERE "type" = 'view'`;
|
||||
const dropViewQueries: ObjectLiteral[] = await this.query(selectViewDropsQuery);
|
||||
await Promise.all(dropViewQueries.map(q => this.query(q["query"])));
|
||||
|
||||
const selectTableDropsQuery = `SELECT 'DROP TABLE "' || name || '";' as query FROM "sqlite_master" WHERE "type" = 'table' AND "name" != 'sqlite_sequence'`;
|
||||
const selectTableDropsQuery = dbPath ? `SELECT 'DROP TABLE "${dbPath}"."' || name || '";' as query FROM "${dbPath}"."sqlite_master" WHERE "type" = 'table' AND "name" != 'sqlite_sequence'` : `SELECT 'DROP TABLE "' || name || '";' as query FROM "sqlite_master" WHERE "type" = 'table' AND "name" != 'sqlite_sequence'`;
|
||||
const dropTableQueries: ObjectLiteral[] = await this.query(selectTableDropsQuery);
|
||||
await Promise.all(dropTableQueries.map(q => this.query(q["query"])));
|
||||
await this.commitTransaction();
|
||||
@ -778,6 +784,21 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen
|
||||
});
|
||||
}
|
||||
|
||||
protected async loadTableRecords(tablePath: string, tableOrIndex: "table" | "index") {
|
||||
let database: string | undefined = undefined
|
||||
const [schema, tableName] = this.splitTablePath(tablePath);
|
||||
if (schema && this.driver.getAttachedDatabasePathRelativeByHandle(schema)) {
|
||||
database = this.driver.getAttachedDatabasePathRelativeByHandle(schema)
|
||||
}
|
||||
const res = await this.query(`SELECT ${database ? `'${database}'` : null} as database, ${schema ? `'${schema}'` : null} as schema, * FROM ${schema ? `"${schema}".` : ""}${this.escapePath(`sqlite_master`)} WHERE "type" = '${tableOrIndex}' AND "${tableOrIndex === "table" ? "name" : "tbl_name"}" IN ('${tableName}')`);
|
||||
return res;
|
||||
}
|
||||
protected async loadPragmaRecords(tablePath: string, pragma: string) {
|
||||
const [, tableName] = this.splitTablePath(tablePath);
|
||||
const res = await this.query(`PRAGMA ${pragma}("${tableName}")`);
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all tables (with given names) from the database and creates a Table from them.
|
||||
*/
|
||||
@ -787,15 +808,18 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen
|
||||
return [];
|
||||
}
|
||||
|
||||
const dbTables: { name: string, sql: string }[] = [];
|
||||
let dbTables: { database?: string, name: string, sql: string }[] = [];
|
||||
let dbIndicesDef: ObjectLiteral[];
|
||||
|
||||
if (!tableNames) {
|
||||
const tablesSql = `SELECT * FROM "sqlite_master" WHERE "type" = 'table'`;
|
||||
dbTables.push(...await this.query(tablesSql))
|
||||
} else {
|
||||
const tableNamesString = tableNames.map(tableName => `'${tableName}'`).join(", ");
|
||||
const tablesSql = `SELECT * FROM "sqlite_master" WHERE "type" = 'table' AND "name" IN (${tableNamesString})`;
|
||||
dbTables.push(...await this.query(tablesSql));
|
||||
|
||||
const tableNamesString = dbTables.map(({ name }) => `'${name}'`).join(", ");
|
||||
dbIndicesDef = await this.query(`SELECT * FROM "sqlite_master" WHERE "type" = 'index' AND "tbl_name" IN (${tableNamesString})`);
|
||||
} else {
|
||||
dbTables = (await Promise.all(tableNames.map(tableName => this.loadTableRecords(tableName, "table")))).reduce((acc, res) => ([...acc, ...res]), []).filter(Boolean);
|
||||
dbIndicesDef = (await Promise.all((tableNames ?? []).map(tableName => this.loadTableRecords(tableName, "index")))).reduce((acc, res) => ([...acc, ...res]), []).filter(Boolean);
|
||||
}
|
||||
|
||||
// if tables were not found in the db, no need to proceed
|
||||
@ -803,23 +827,18 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen
|
||||
return [];
|
||||
}
|
||||
|
||||
// load indices
|
||||
const tableNamesString = dbTables.map(({ name }) => `'${name}'`).join(", ");
|
||||
const dbIndicesDef: ObjectLiteral[] = await this.query(`SELECT * FROM "sqlite_master" WHERE "type" = 'index' AND "tbl_name" IN (${tableNamesString})`);
|
||||
|
||||
// create table schemas for loaded tables
|
||||
return Promise.all(dbTables.map(async dbTable => {
|
||||
const table = new Table();
|
||||
|
||||
table.name = dbTable["name"];
|
||||
const tablePath = dbTable['database'] && this.driver.getAttachedDatabaseHandleByRelativePath(dbTable['database']) ? `${this.driver.getAttachedDatabaseHandleByRelativePath(dbTable['database'])}.${dbTable['name']}` : dbTable['name']
|
||||
const table = new Table({name: tablePath});
|
||||
|
||||
const sql = dbTable["sql"];
|
||||
|
||||
// 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"]}")`),
|
||||
this.loadPragmaRecords(tablePath, `table_info`),
|
||||
this.loadPragmaRecords(tablePath, `index_list`),
|
||||
this.loadPragmaRecords(tablePath, `foreign_key_list`),
|
||||
]);
|
||||
|
||||
// find column name with auto increment
|
||||
@ -937,7 +956,6 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen
|
||||
const indexColumns = indexInfos
|
||||
.sort((indexInfo1, indexInfo2) => parseInt(indexInfo1["seqno"]) - parseInt(indexInfo2["seqno"]))
|
||||
.map(indexInfo => indexInfo["name"]);
|
||||
|
||||
if (indexColumns.length === 1) {
|
||||
const column = table.columns.find(column => {
|
||||
return !!indexColumns.find(indexColumn => indexColumn === column.name);
|
||||
@ -950,8 +968,8 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen
|
||||
const foundMapping = uniqueMappings.find(mapping => {
|
||||
return mapping!.columns.every(column =>
|
||||
indexColumns.indexOf(column) !== -1
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
return new TableUnique({
|
||||
name: foundMapping ? foundMapping.name : this.connection.namingStrategy.uniqueConstraintName(table, indexColumns),
|
||||
@ -981,11 +999,12 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen
|
||||
const indexColumns = indexInfos
|
||||
.sort((indexInfo1, indexInfo2) => parseInt(indexInfo1["seqno"]) - parseInt(indexInfo2["seqno"]))
|
||||
.map(indexInfo => indexInfo["name"]);
|
||||
const dbIndexPath = `${dbTable["database"] ? `${dbTable["database"]}.` : ''}${dbIndex!["name"]}`;
|
||||
|
||||
const isUnique = dbIndex!["unique"] === "1" || dbIndex!["unique"] === 1;
|
||||
return new TableIndex(<TableIndexOptions>{
|
||||
table: table,
|
||||
name: dbIndex!["name"],
|
||||
name: dbIndexPath,
|
||||
columnNames: indexColumns,
|
||||
isUnique: isUnique,
|
||||
where: condition ? condition[1] : undefined
|
||||
@ -1010,7 +1029,8 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen
|
||||
throw new TypeORMError(`Sqlite does not support AUTOINCREMENT on composite primary key`);
|
||||
|
||||
const columnDefinitions = table.columns.map(column => this.buildCreateColumnSql(column, skipPrimary)).join(", ");
|
||||
let sql = `CREATE TABLE "${table.name}" (${columnDefinitions}`;
|
||||
const [database] = this.splitTablePath(table.name);
|
||||
let sql = `CREATE TABLE ${this.escapePath(table.name)} (${columnDefinitions}`;
|
||||
|
||||
// need for `addColumn()` method, because it recreates table.
|
||||
table.columns
|
||||
@ -1044,13 +1064,21 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen
|
||||
}
|
||||
|
||||
if (table.foreignKeys.length > 0 && createForeignKeys) {
|
||||
const foreignKeysSql = table.foreignKeys.map(fk => {
|
||||
const foreignKeysSql = table.foreignKeys.filter(fk => {
|
||||
const [referencedDatabase] = this.splitTablePath(fk.referencedTableName);
|
||||
if (referencedDatabase !== database) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(fk => {
|
||||
const [, referencedTable] = this.splitTablePath(fk.referencedTableName);
|
||||
const columnNames = fk.columnNames.map(columnName => `"${columnName}"`).join(", ");
|
||||
if (!fk.name)
|
||||
fk.name = this.connection.namingStrategy.foreignKeyName(table, fk.columnNames, this.getTablePath(fk), fk.referencedColumnNames);
|
||||
const referencedColumnNames = fk.referencedColumnNames.map(columnName => `"${columnName}"`).join(", ");
|
||||
|
||||
let constraint = `CONSTRAINT "${fk.name}" FOREIGN KEY (${columnNames}) REFERENCES "${fk.referencedTableName}" (${referencedColumnNames})`;
|
||||
let constraint = `CONSTRAINT "${fk.name}" FOREIGN KEY (${columnNames}) REFERENCES "${referencedTable}" (${referencedColumnNames})`;
|
||||
if (fk.onDelete)
|
||||
constraint += ` ON DELETE ${fk.onDelete}`;
|
||||
if (fk.onUpdate)
|
||||
@ -1082,7 +1110,7 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen
|
||||
*/
|
||||
protected dropTableSql(tableOrName: Table|string, ifExist?: boolean): Query {
|
||||
const tableName = tableOrName instanceof Table ? tableOrName.name : tableOrName;
|
||||
const query = ifExist ? `DROP TABLE IF EXISTS "${tableName}"` : `DROP TABLE "${tableName}"`;
|
||||
const query = ifExist ? `DROP TABLE IF EXISTS ${this.escapePath(tableName)}` : `DROP TABLE ${this.escapePath(tableName)}`;
|
||||
return new Query(query);
|
||||
}
|
||||
|
||||
@ -1124,7 +1152,8 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen
|
||||
*/
|
||||
protected createIndexSql(table: Table, index: TableIndex): Query {
|
||||
const columns = index.columnNames.map(columnName => `"${columnName}"`).join(", ");
|
||||
return new Query(`CREATE ${index.isUnique ? "UNIQUE " : ""}INDEX "${index.name}" ON "${table.name}" (${columns}) ${index.where ? "WHERE " + index.where : ""}`);
|
||||
const [database, tableName] = this.splitTablePath(table.name);
|
||||
return new Query(`CREATE ${index.isUnique ? "UNIQUE " : ""}INDEX ${database ? `"${database}".` : ""}${this.escapePath(index.name!)} ON "${tableName}" (${columns}) ${index.where ? "WHERE " + index.where : ""}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1132,7 +1161,7 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen
|
||||
*/
|
||||
protected dropIndexSql(indexOrName: TableIndex|string): Query {
|
||||
let indexName = indexOrName instanceof TableIndex ? indexOrName.name : indexOrName;
|
||||
return new Query(`DROP INDEX "${indexName}"`);
|
||||
return new Query(`DROP INDEX ${this.escapePath(indexName!)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1173,7 +1202,9 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen
|
||||
});
|
||||
|
||||
// change table name into 'temporary_table'
|
||||
newTable.name = "temporary_" + newTable.name;
|
||||
let [databaseNew, tableNameNew] = this.splitTablePath(newTable.name);
|
||||
let [, tableNameOld] = this.splitTablePath(oldTable.name);
|
||||
newTable.name = tableNameNew = `${databaseNew ? `${databaseNew}.` : ""}temporary_${tableNameNew}`;
|
||||
|
||||
// create new table
|
||||
upQueries.push(this.createTableSql(newTable, true));
|
||||
@ -1194,8 +1225,8 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen
|
||||
}).map(column => `"${column.name}"`).join(", ");
|
||||
}
|
||||
|
||||
upQueries.push(new Query(`INSERT INTO "${newTable.name}"(${newColumnNames}) SELECT ${oldColumnNames} FROM "${oldTable.name}"`));
|
||||
downQueries.push(new Query(`INSERT INTO "${oldTable.name}"(${oldColumnNames}) SELECT ${newColumnNames} FROM "${newTable.name}"`));
|
||||
upQueries.push(new Query(`INSERT INTO ${this.escapePath(newTable.name)}(${newColumnNames}) SELECT ${oldColumnNames} FROM ${this.escapePath(oldTable.name)}`));
|
||||
downQueries.push(new Query(`INSERT INTO ${this.escapePath(oldTable.name)}(${oldColumnNames}) SELECT ${newColumnNames} FROM ${this.escapePath(newTable.name)}`));
|
||||
}
|
||||
|
||||
// drop old table
|
||||
@ -1203,8 +1234,8 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen
|
||||
downQueries.push(this.createTableSql(oldTable, true));
|
||||
|
||||
// rename old table
|
||||
upQueries.push(new Query(`ALTER TABLE "${newTable.name}" RENAME TO "${oldTable.name}"`));
|
||||
downQueries.push(new Query(`ALTER TABLE "${oldTable.name}" RENAME TO "${newTable.name}"`));
|
||||
upQueries.push(new Query(`ALTER TABLE ${this.escapePath(newTable.name)} RENAME TO ${this.escapePath(tableNameOld)}`));
|
||||
downQueries.push(new Query(`ALTER TABLE ${this.escapePath(oldTable.name)} RENAME TO ${this.escapePath(tableNameNew)}`));
|
||||
|
||||
newTable.name = oldTable.name;
|
||||
|
||||
@ -1221,4 +1252,19 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen
|
||||
this.replaceCachedTable(oldTable, newTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* tablePath e.g. "myDB.myTable", "myTable"
|
||||
*/
|
||||
protected splitTablePath(tablePath: string): [string | undefined, string] {
|
||||
return ((tablePath.indexOf(".") !== -1) ? tablePath.split(".") : [undefined, tablePath]) as [string | undefined, string];
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes given table or view path. Tolerates leading/trailing dots
|
||||
*/
|
||||
protected escapePath(target: Table|View|string, disableEscape?: boolean): string {
|
||||
const tableName = target instanceof Table || target instanceof View ? target.name : target;
|
||||
return tableName.replace(/^\.+|\.+$/g, "").split(".").map(i => disableEscape ? i : `"${i}"`).join(".");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -10,6 +10,8 @@ import { ColumnType } from "../types/ColumnTypes";
|
||||
import { QueryRunner } from "../../query-runner/QueryRunner";
|
||||
import { AbstractSqliteDriver } from "../sqlite-abstract/AbstractSqliteDriver";
|
||||
import {ReplicationMode} from "../types/ReplicationMode";
|
||||
import {filepathToName, isAbsolute} from "../../util/PathUtils";
|
||||
|
||||
|
||||
/**
|
||||
* Organizes communication with sqlite DBMS.
|
||||
@ -81,6 +83,34 @@ export class SqliteDriver extends AbstractSqliteDriver {
|
||||
return super.normalizeType(column);
|
||||
}
|
||||
|
||||
async afterConnect(): Promise<void> {
|
||||
return this.attachDatabases();
|
||||
}
|
||||
|
||||
/**
|
||||
* For SQLite, the database may be added in the decorator metadata. It will be a filepath to a database file.
|
||||
*/
|
||||
buildTableName(tableName: string, _schema?: string, database?: string): string {
|
||||
|
||||
if (!database) return tableName;
|
||||
if (this.getAttachedDatabaseHandleByRelativePath(database)) return `${this.getAttachedDatabaseHandleByRelativePath(database)}.${tableName}`;
|
||||
|
||||
if (database === this.options.database) return tableName;
|
||||
|
||||
// we use the decorated name as supplied when deriving attach handle (ideally without non-portable absolute path)
|
||||
const identifierHash = filepathToName(database);
|
||||
// decorated name will be assumed relative to main database file when non absolute. Paths supplied as absolute won't be portable
|
||||
const absFilepath = isAbsolute(database) ? database : path.join(this.getMainDatabasePath(), database);
|
||||
|
||||
this.attachedDatabases[database] = {
|
||||
attachFilepathAbsolute: absFilepath,
|
||||
attachFilepathRelative: database,
|
||||
attachHandle: identifierHash,
|
||||
};
|
||||
|
||||
return `${identifierHash}.${tableName}`;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Protected Methods
|
||||
// -------------------------------------------------------------------------
|
||||
@ -144,4 +174,24 @@ export class SqliteDriver extends AbstractSqliteDriver {
|
||||
await mkdirp(path.dirname(fullPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the attaching of the database files. The attachedDatabase should have been populated during calls to #buildTableName
|
||||
* during EntityMetadata production (see EntityMetadata#buildTablePath)
|
||||
*
|
||||
* https://sqlite.org/lang_attach.html
|
||||
*/
|
||||
protected async attachDatabases() {
|
||||
|
||||
// @todo - possibly check number of databases (but unqueriable at runtime sadly) - https://www.sqlite.org/limits.html#max_attached
|
||||
for await (const {attachHandle, attachFilepathAbsolute} of Object.values(this.attachedDatabases)) {
|
||||
await this.createDatabaseDirectory(attachFilepathAbsolute);
|
||||
await this.connection.query(`ATTACH "${attachFilepathAbsolute}" AS "${attachHandle}"`);
|
||||
}
|
||||
}
|
||||
|
||||
protected getMainDatabasePath(): string {
|
||||
const optionsDb = this.options.database;
|
||||
return path.dirname(isAbsolute(optionsDb) ? optionsDb : path.join(process.cwd(), optionsDb));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
32
src/util/PathUtils.ts
Normal file
32
src/util/PathUtils.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { hash } from "./StringUtils";
|
||||
|
||||
const WINDOWS_PATH_REGEXP = /^([a-zA-Z]:.*)$/;
|
||||
const UNC_WINDOWS_PATH_REGEXP = /^\\\\(\.\\)?(.*)$/;
|
||||
|
||||
export function toPortablePath(filepath: string): string {
|
||||
if (process.platform !== `win32`)
|
||||
return filepath;
|
||||
|
||||
if (filepath.match(WINDOWS_PATH_REGEXP))
|
||||
filepath = filepath.replace(WINDOWS_PATH_REGEXP, `/$1`);
|
||||
else if (filepath.match(UNC_WINDOWS_PATH_REGEXP))
|
||||
filepath = filepath.replace(UNC_WINDOWS_PATH_REGEXP, (match, p1, p2) => `/unc/${p1 ? `.dot/` : ``}${p2}`);
|
||||
|
||||
return filepath.replace(/\\/g, `/`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create deterministic valid database name (class, database) of fixed length from any filepath. Equivalent paths for windows/posix systems should
|
||||
* be equivalent to enable portability
|
||||
*/
|
||||
export function filepathToName(filepath: string): string {
|
||||
const uniq = toPortablePath(filepath).toLowerCase();
|
||||
return hash(uniq, {length: 63});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cross platform isAbsolute
|
||||
*/
|
||||
export function isAbsolute(filepath: string): boolean {
|
||||
return !!filepath.match(/^(?:[a-z]:|[\\]|[\/])/i);
|
||||
}
|
||||
@ -75,6 +75,7 @@ describe("column kinds > create date column", () => {
|
||||
expect(loadedPostAfterUpdate!.createdAt.toString()).to.be.eql(loadedPostBeforeUpdate!.createdAt.toString());
|
||||
})));
|
||||
|
||||
|
||||
it("create date column should set a custom date when specified", () => Promise.all(connections.map(async connection => {
|
||||
const postRepository = connection.getRepository(Post);
|
||||
|
||||
|
||||
@ -94,5 +94,5 @@ describe("ConnectionOptionsReader", () => {
|
||||
const connectionOptionsReader = new ConnectionOptionsReader({ root: path.join(__dirname, "configs/yaml"), configName: "test-yaml" });
|
||||
const fileOptions: ConnectionOptions = await connectionOptionsReader.get("file");
|
||||
expect(fileOptions.database).to.have.string("/test-yaml");
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import {Entity} from "../../../../../src/decorator/entity/Entity";
|
||||
import {Column} from "../../../../../src/decorator/columns/Column";
|
||||
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
|
||||
|
||||
@Entity({ database: "filename-sqlite.db" })
|
||||
export class Answer {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
text: string;
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import {Entity} from "../../../../../src/decorator/entity/Entity";
|
||||
import {Column} from "../../../../../src/decorator/columns/Column";
|
||||
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
|
||||
import {ManyToOne} from "../../../../../src/decorator/relations/ManyToOne";
|
||||
import {Post} from "./Post";
|
||||
|
||||
@Entity({ database: "./subdir/relative-subdir-sqlite.db" })
|
||||
export class Category {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@ManyToOne(type => Post)
|
||||
post: Post;
|
||||
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import {Entity} from "../../../../../src/decorator/entity/Entity";
|
||||
import {Column} from "../../../../../src/decorator/columns/Column";
|
||||
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
|
||||
|
||||
@Entity({ database: "./subdir/relative-subdir-sqlite.db" })
|
||||
export class Post {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import {Entity} from "../../../../../src/decorator/entity/Entity";
|
||||
import {Column} from "../../../../../src/decorator/columns/Column";
|
||||
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
|
||||
|
||||
@Entity()
|
||||
export class User {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
}
|
||||
@ -0,0 +1,149 @@
|
||||
import "reflect-metadata";
|
||||
import { expect } from 'chai';
|
||||
import { Connection } from "../../../../src/connection/Connection";
|
||||
import {
|
||||
closeTestingConnections,
|
||||
createTestingConnections,
|
||||
reloadTestingDatabases,
|
||||
} from "../../../utils/test-utils";
|
||||
import { Answer } from "./entity/Answer";
|
||||
import { Category } from "./entity/Category";
|
||||
import { Post } from "./entity/Post";
|
||||
import { User } from "./entity/User";
|
||||
import { filepathToName } from "../../../../src/util/PathUtils";
|
||||
import rimraf from "rimraf";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
const VALID_NAME_REGEX = /^(?!sqlite_).{1,63}$/
|
||||
|
||||
describe("multi-database > basic-functionality", () => {
|
||||
|
||||
describe("filepathToName()", () => {
|
||||
for (const platform of [`darwin`, `win32`]) {
|
||||
let realPlatform: string;
|
||||
|
||||
beforeEach(() => {
|
||||
realPlatform = process.platform;
|
||||
Object.defineProperty(process, `platform`, {
|
||||
configurable: true,
|
||||
value: platform,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, `platform`, {
|
||||
configurable: true,
|
||||
value: realPlatform,
|
||||
});
|
||||
});
|
||||
|
||||
it(`produces deterministic, unique, and valid table names for relative paths; leaves absolute paths unchanged (${platform})`, () => {
|
||||
const testMap = [
|
||||
["FILENAME.db", "filename.db"],
|
||||
["..\\FILENAME.db", "../filename.db"],
|
||||
["..\\longpathdir\\longpathdir\\longpathdir\\longpathdir\\longpathdir\\longpathdir\\longpathdir\\FILENAME.db", "../longpathdir/longpathdir/longpathdir/longpathdir/longpathdir/longpathdir/longpathdir/filename.db"],
|
||||
["C:\\dir\FILENAME.db", "C:\\dir\FILENAME.db"],
|
||||
["/dir/filename.db", "/dir/filename.db"],
|
||||
];
|
||||
for (const [winOs, otherOs] of testMap) {
|
||||
const winOsRes = filepathToName(winOs);
|
||||
const otherOsRes = filepathToName(otherOs);
|
||||
expect(winOsRes).to.equal(otherOsRes);
|
||||
expect(winOsRes).to.match(VALID_NAME_REGEX, `'${winOs}' is invalid table name`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("multiple databases", () => {
|
||||
|
||||
let connections: Connection[];
|
||||
const tempPath = path.resolve(__dirname, "../../../../../../temp");
|
||||
const attachAnswerPath = path.join(tempPath, "filename-sqlite.db");
|
||||
const attachAnswerHandle = filepathToName("filename-sqlite.db");
|
||||
const attachCategoryPath = path.join(tempPath, "./subdir/relative-subdir-sqlite.db");
|
||||
const attachCategoryHandle = filepathToName("./subdir/relative-subdir-sqlite.db");
|
||||
|
||||
before(async () => {
|
||||
connections = await createTestingConnections({
|
||||
entities: [Answer, Category, Post, User],
|
||||
// enabledDrivers: ["sqlite", "better-sqlite3"],
|
||||
enabledDrivers: ["sqlite"],
|
||||
name: "sqlite",
|
||||
});
|
||||
});
|
||||
beforeEach(() => reloadTestingDatabases(connections));
|
||||
after(async () => {
|
||||
await closeTestingConnections(connections);
|
||||
return new Promise(resolve => rimraf(`${tempPath}/**/*.db`, {}, () => resolve()));
|
||||
});
|
||||
|
||||
it("should correctly attach and create database files", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
const expectedMainPath = path.join(tempPath, (connections[0].options.database as string).match(/^.*[\\|\/](?<filename>[^\\|\/]+)$/)!.groups!["filename"]);
|
||||
|
||||
expect(fs.existsSync(expectedMainPath)).to.be.true;
|
||||
expect(fs.existsSync(attachAnswerPath)).to.be.true;
|
||||
expect(fs.existsSync(attachCategoryPath)).to.be.true;
|
||||
})));
|
||||
|
||||
it("should prefix tableName when custom database used in Entity decorator", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
const queryRunner = connection.createQueryRunner();
|
||||
|
||||
const tablePathAnswer = `${attachAnswerHandle}.answer`;
|
||||
const table = await queryRunner.getTable(tablePathAnswer);
|
||||
await queryRunner.release();
|
||||
|
||||
const answer = new Answer();
|
||||
answer.text = "Answer #1";
|
||||
|
||||
await connection.getRepository(Answer).save(answer);
|
||||
|
||||
const sql = connection.createQueryBuilder(Answer, "answer")
|
||||
.where("answer.id = :id", {id: 1})
|
||||
.getSql();
|
||||
|
||||
sql.should.be.equal(`SELECT "answer"."id" AS "answer_id", "answer"."text" AS "answer_text" FROM "${attachAnswerHandle}"."answer" "answer" WHERE "answer"."id" = ?`);
|
||||
table!.name.should.be.equal(tablePathAnswer);
|
||||
})));
|
||||
|
||||
it("should not affect tableName when using default main database", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
const queryRunner = connection.createQueryRunner();
|
||||
|
||||
const tablePathUser = `user`;
|
||||
const table = await queryRunner.getTable(tablePathUser);
|
||||
await queryRunner.release();
|
||||
|
||||
const user = new User();
|
||||
user.name = "User #1";
|
||||
await connection.getRepository(User).save(user);
|
||||
|
||||
const sql = connection.createQueryBuilder(User, "user")
|
||||
.where("user.id = :id", {id: 1})
|
||||
.getSql();
|
||||
|
||||
sql.should.be.equal(`SELECT "user"."id" AS "user_id", "user"."name" AS "user_name" FROM "user" "user" WHERE "user"."id" = ?`);
|
||||
|
||||
table!.name.should.be.equal(tablePathUser);
|
||||
})));
|
||||
|
||||
it("should create foreign keys for relations within the same database", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
const queryRunner = connection.createQueryRunner();
|
||||
const tablePathCategory = `${attachCategoryHandle}.category`;
|
||||
const tablePathPost = `${attachCategoryHandle}.post`;
|
||||
const tableCategory = (await queryRunner.getTable(tablePathCategory))!;
|
||||
const tablePost = (await queryRunner.getTable(tablePathPost))!;
|
||||
await queryRunner.release();
|
||||
|
||||
expect(tableCategory.foreignKeys.length).to.eq(1);
|
||||
expect(tableCategory.foreignKeys[0].columnNames.length).to.eq(1); // before the fix this was 2, one for each schema
|
||||
expect(tableCategory.foreignKeys[0].columnNames[0]).to.eq("postId");
|
||||
|
||||
expect(tablePost.foreignKeys.length).to.eq(0);
|
||||
})));
|
||||
});
|
||||
});
|
||||
@ -3,7 +3,7 @@ import * as assert from "assert";
|
||||
import {createConnection, getConnectionOptions} from "../../../src/index";
|
||||
import {Connection} from "../../../src/connection/Connection";
|
||||
|
||||
describe("github issues > #798 sqlite: 'database' path in ormconfig.json is not relative", () => {
|
||||
describe.only("github issues > #798 sqlite: 'database' path in ormconfig.json is not relative", () => {
|
||||
let connection: Connection;
|
||||
const oldCwd = process.cwd();
|
||||
|
||||
|
||||
162
test/unit/path-utils.ts
Normal file
162
test/unit/path-utils.ts
Normal file
@ -0,0 +1,162 @@
|
||||
import { toPortablePath, isAbsolute } from "../../src/util/PathUtils";
|
||||
import { expect } from "chai";
|
||||
|
||||
describe(`path-utils`, () => {
|
||||
describe("isAbsolute", () => {
|
||||
it("discriminates cross platform relative paths", () => {
|
||||
const testMap: [string, boolean][] = [
|
||||
["FILENAME.db", false],
|
||||
["./FILENAME.db", false],
|
||||
[".FILENAME.db", false],
|
||||
["path/FILENAME.db", false],
|
||||
["pathFILENAME.db", false],
|
||||
["..FILENAME.db", false],
|
||||
["../filename.db", false],
|
||||
["C:\\dirFILENAME.db", true],
|
||||
["/dir/filename.db", true],
|
||||
];
|
||||
for (const [aPath, expected] of testMap) {
|
||||
expect(isAbsolute(aPath), `${aPath} did not match ${expected}`).to.equal(expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("toPortablePath", () => {
|
||||
for (const platform of [`darwin`, `win32`]) {
|
||||
let realPlatform: string;
|
||||
|
||||
describe(`Platform ${platform}`, () => {
|
||||
beforeEach(() => {
|
||||
realPlatform = process.platform;
|
||||
Object.defineProperty(process, `platform`, {
|
||||
configurable: true,
|
||||
value: platform,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, `platform`, {
|
||||
configurable: true,
|
||||
value: realPlatform,
|
||||
});
|
||||
});
|
||||
|
||||
describe(`toPortablePath`, () => {
|
||||
if (platform !== `win32`) {
|
||||
it(`should change paths on non-Windows platform`, () => {
|
||||
const inputPath = `C:\\Users\\user\\proj`;
|
||||
const outputPath = inputPath;
|
||||
expect(toPortablePath(inputPath)).to.equal(
|
||||
outputPath
|
||||
);
|
||||
});
|
||||
} else {
|
||||
it(`shouldn't change absolute posix paths when producing portable path`, () => {
|
||||
const inputPath = `/home/user/proj`;
|
||||
const outputPath = inputPath;
|
||||
expect(toPortablePath(inputPath)).to.equal(
|
||||
outputPath
|
||||
);
|
||||
});
|
||||
|
||||
it(`shouldn't change absolute paths that are already portable`, () => {
|
||||
const inputPath = `/c:/Users/user/proj`;
|
||||
const outputPath = `/c:/Users/user/proj`;
|
||||
expect(toPortablePath(inputPath)).to.equal(
|
||||
outputPath
|
||||
);
|
||||
});
|
||||
|
||||
it(`should normalize the slashes in relative Windows paths`, () => {
|
||||
const inputPath = `..\\Users\\user/proj`;
|
||||
const outputPath = `../Users/user/proj`;
|
||||
expect(toPortablePath(inputPath)).to.equal(
|
||||
outputPath
|
||||
);
|
||||
});
|
||||
|
||||
it(`should transform Windows paths into their posix counterparts (uppercase drive)`, () => {
|
||||
const inputPath = `C:\\Users\\user\\proj`;
|
||||
const outputPath = `/C:/Users/user/proj`;
|
||||
expect(toPortablePath(inputPath)).to.equal(
|
||||
outputPath
|
||||
);
|
||||
});
|
||||
|
||||
it(`should transform Windows paths into their posix counterparts (lowercase drive)`, () => {
|
||||
const inputPath = `c:\\Users\\user\\proj`;
|
||||
const outputPath = `/c:/Users/user/proj`;
|
||||
expect(toPortablePath(inputPath)).to.equal(
|
||||
outputPath
|
||||
);
|
||||
});
|
||||
|
||||
it(`should transform Windows paths into their posix counterparts (forward slashes)`, () => {
|
||||
const inputPath = `C:/Users/user/proj`;
|
||||
const outputPath = `/C:/Users/user/proj`;
|
||||
expect(toPortablePath(inputPath)).to.equal(
|
||||
outputPath
|
||||
);
|
||||
});
|
||||
|
||||
it(`should support Windows paths that contain both backslashes and forward slashes`, () => {
|
||||
const inputPath = `C:/Users\\user/proj`;
|
||||
const outputPath = `/C:/Users/user/proj`;
|
||||
expect(toPortablePath(inputPath)).to.equal(
|
||||
outputPath
|
||||
);
|
||||
});
|
||||
|
||||
it(`should support drive: Windows paths`, () => {
|
||||
const inputPath = `C:`;
|
||||
const outputPath = `/C:`;
|
||||
expect(toPortablePath(inputPath)).to.equal(
|
||||
outputPath
|
||||
);
|
||||
});
|
||||
|
||||
it(`should support UNC Windows paths (\\\\[server]\\[sharename]\\)`, () => {
|
||||
const inputPath = `\\\\Server01\\user\\docs\\Letter.txt`;
|
||||
const outputPath = `/unc/Server01/user/docs/Letter.txt`;
|
||||
expect(toPortablePath(inputPath)).to.equal(
|
||||
outputPath
|
||||
);
|
||||
});
|
||||
|
||||
it(`should support Long UNC Windows paths (\\\\?\\[server]\\[sharename]\\)`, () => {
|
||||
const inputPath = `\\\\?\\Server01\\user\\docs\\Letter.txt`;
|
||||
const outputPath = `/unc/?/Server01/user/docs/Letter.txt`;
|
||||
expect(toPortablePath(inputPath)).to.equal(
|
||||
outputPath
|
||||
);
|
||||
});
|
||||
|
||||
it(`should support Long UNC Windows paths (\\\\?\\UNC\\[server]\\[sharename]\\)`, () => {
|
||||
const inputPath = `\\\\?\\UNC\\Server01\\user\\docs\\Letter.txt`;
|
||||
const outputPath = `/unc/?/UNC/Server01/user/docs/Letter.txt`;
|
||||
expect(toPortablePath(inputPath)).to.equal(
|
||||
outputPath
|
||||
);
|
||||
});
|
||||
|
||||
it(`should support Long UNC Windows paths (\\\\?\\[drive_spec]:\\)`, () => {
|
||||
const inputPath = `\\\\?\\C:\\user\\docs\\Letter.txt`;
|
||||
const outputPath = `/unc/?/C:/user/docs/Letter.txt`;
|
||||
expect(toPortablePath(inputPath)).to.equal(
|
||||
outputPath
|
||||
);
|
||||
});
|
||||
|
||||
it(`should support Long UNC Windows paths with dot (\\\\.\\[physical_device]\\)`, () => {
|
||||
const inputPath = `\\\\.\\PhysicalDevice\\user\\docs\\Letter.txt`;
|
||||
const outputPath = `/unc/.dot/PhysicalDevice/user/docs/Letter.txt`;
|
||||
expect(toPortablePath(inputPath)).to.equal(
|
||||
outputPath
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -7,6 +7,7 @@ import {NamingStrategyInterface} from "../../src/naming-strategy/NamingStrategyI
|
||||
import {QueryResultCache} from "../../src/cache/QueryResultCache";
|
||||
import {Logger} from "../../src/logger/Logger";
|
||||
import {CockroachDriver} from "../../src/driver/cockroachdb/CockroachDriver";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* Interface in which data is stored in ormconfig.json of the project.
|
||||
@ -172,16 +173,16 @@ export function setupSingleTestingConnection(driverType: DatabaseType, options:
|
||||
/**
|
||||
* Loads test connection options from ormconfig.json file.
|
||||
*/
|
||||
export function getTypeOrmConfig(): TestingConnectionOptions[] {
|
||||
function getOrmFilepath(): string {
|
||||
try {
|
||||
|
||||
try {
|
||||
// first checks build/compiled
|
||||
// useful for docker containers in order to provide a custom config
|
||||
return require(__dirname + "/../../ormconfig.json");
|
||||
return require.resolve(__dirname + "/../../ormconfig.json");
|
||||
} catch (err) {
|
||||
// fallbacks to the root config
|
||||
return require(__dirname + "/../../../../ormconfig.json");
|
||||
return require.resolve(__dirname + "/../../../../ormconfig.json");
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
@ -191,6 +192,10 @@ export function getTypeOrmConfig(): TestingConnectionOptions[] {
|
||||
}
|
||||
}
|
||||
|
||||
export function getTypeOrmConfig(): TestingConnectionOptions[] {
|
||||
return require(getOrmFilepath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a testing connections options based on the configuration in the ormconfig.json
|
||||
* and given options that can override some of its configuration for the test-specific use case.
|
||||
@ -241,6 +246,9 @@ export function setupTestingConnections(options?: TestingOptions): ConnectionOpt
|
||||
newOptions.namingStrategy = options.namingStrategy;
|
||||
if (options && options.metadataTableName)
|
||||
newOptions.metadataTableName = options.metadataTableName;
|
||||
|
||||
newOptions.baseDirectory = path.dirname(getOrmFilepath());
|
||||
|
||||
return newOptions;
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user