Merge pull request #851 from iz-iznogood/indices-fixes

Fixes for issues #833, #834 and #840
This commit is contained in:
Umed Khudoiberdiev 2017-09-11 08:56:56 +05:00 committed by GitHub
commit b84d95b902
16 changed files with 325 additions and 19 deletions

View File

@ -49,7 +49,8 @@ export function Index(nameOrFieldsOrOptions?: string|string[]|((object: any) =>
target: propertyName ? clsOrObject.constructor : clsOrObject as Function,
name: name,
columns: propertyName ? [propertyName] : fields,
unique: options && options.unique ? true : false
unique: options && options.unique ? true : false,
sparse: options && options.sparse ? true : false
};
getMetadataArgsStorage().indices.push(args);
};

View File

@ -327,7 +327,7 @@ export class MysqlQueryRunner implements QueryRunner {
const tableNamesString = tableNames.map(tableName => `'${tableName}'`).join(", ");
const tablesSql = `SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = '${this.dbName}' AND TABLE_NAME IN (${tableNamesString})`;
const columnsSql = `SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '${this.dbName}'`;
const indicesSql = `SELECT * FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = '${this.dbName}' AND INDEX_NAME != 'PRIMARY'`;
const indicesSql = `SELECT * FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = '${this.dbName}' AND INDEX_NAME != 'PRIMARY' ORDER BY SEQ_IN_INDEX`;
const foreignKeysSql = `SELECT * FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = '${this.dbName}' AND REFERENCED_COLUMN_NAME IS NOT NULL`;
const [dbTables, dbColumns, dbIndices, dbForeignKeys]: ObjectLiteral[][] = await Promise.all([
this.query(tablesSql),
@ -425,7 +425,7 @@ export class MysqlQueryRunner implements QueryRunner {
}
}
return new IndexSchema(dbTable["TABLE_NAME"], dbIndexName, columnNames, false /* todo: uniqueness */);
return new IndexSchema(dbTable["TABLE_NAME"], dbIndexName, columnNames, currentDbIndices[0]["NON_UNIQUE"] === 0);
})
.filter(index => !!index) as IndexSchema[]; // remove empty returns

View File

@ -336,7 +336,7 @@ export class PostgresQueryRunner implements QueryRunner {
const tableNamesString = tableNames.map(name => "'" + name + "'").join(", ");
const tablesSql = `SELECT * FROM information_schema.tables WHERE table_catalog = '${this.dbName}' AND table_schema = '${this.schemaName}' AND table_name IN (${tableNamesString})`;
const columnsSql = `SELECT * FROM information_schema.columns WHERE table_catalog = '${this.dbName}' AND table_schema = '${this.schemaName}'`;
const indicesSql = `SELECT t.relname AS table_name, i.relname AS index_name, a.attname AS column_name FROM pg_class t, pg_class i, pg_index ix, pg_attribute a, pg_namespace ns
const indicesSql = `SELECT t.relname AS table_name, i.relname AS index_name, a.attname AS column_name, ix.indisunique AS is_unique, a.attnum, ix.indkey FROM pg_class t, pg_class i, pg_index ix, pg_attribute a, pg_namespace ns
WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid
AND a.attnum = ANY(ix.indkey) AND t.relkind = 'r' AND t.relname IN (${tableNamesString}) AND t.relnamespace = ns.OID AND ns.nspname ='${this.schemaName}' ORDER BY t.relname, i.relname`;
const foreignKeysSql = `SELECT table_name, constraint_name FROM information_schema.table_constraints WHERE table_catalog = '${this.dbName}' AND table_schema = '${this.schemaName}' AND constraint_type = 'FOREIGN KEY'`;
@ -427,11 +427,14 @@ where constraint_type = 'PRIMARY KEY' AND c.table_schema = '${this.schemaName}'
.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"]);
const dbIndicesInfos = dbIndices
.filter(dbIndex => dbIndex["table_name"] === tableSchema.name && dbIndex["index_name"] === dbIndexName);
const columnPositions = dbIndicesInfos[0]["indkey"].split(" ")
.map((x: string) => parseInt(x));
const columnNames = columnPositions
.map((pos: number) => dbIndicesInfos.find(idx => idx.attnum === pos)!["column_name"]);
return new IndexSchema(dbTable["TABLE_NAME"], dbIndexName, columnNames, false /* todo: uniqueness */);
return new IndexSchema(dbTable["TABLE_NAME"], dbIndexName, columnNames, dbIndicesInfos[0]["is_unique"]);
});
return tableSchema;

View File

@ -300,7 +300,9 @@ export class AbstractSqliteQueryRunner implements QueryRunner {
.map(async dbIndexName => {
const dbIndex = dbIndices.find(dbIndex => dbIndex["name"] === dbIndexName);
const indexInfos: ObjectLiteral[] = await this.query(`PRAGMA index_info("${dbIndex!["name"]}")`);
const indexColumns = indexInfos.map(indexInfo => indexInfo["name"]);
const indexColumns = indexInfos
.sort((indexInfo1, indexInfo2) => parseInt(indexInfo1["seqno"]) - parseInt(indexInfo2["seqno"]))
.map(indexInfo => indexInfo["name"]);
// check if db index is generated by sqlite itself and has special use case
if (dbIndex!["name"].substr(0, "sqlite_autoindex".length) === "sqlite_autoindex") {
@ -316,7 +318,8 @@ export class AbstractSqliteQueryRunner implements QueryRunner {
return Promise.resolve(undefined);
} else {
return new IndexSchema(dbTable["name"], dbIndex!["name"], indexColumns, dbIndex!["unique"] === "1");
const isUnique = dbIndex!["unique"] === "1" || dbIndex!["unique"] === 1;
return new IndexSchema(dbTable["name"], dbIndex!["name"], indexColumns, isUnique);
}
});

View File

@ -497,9 +497,9 @@ export class SqlServerQueryRunner implements QueryRunner {
`LEFT JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tableConstraints ON tableConstraints.CONSTRAINT_NAME = columnUsages.CONSTRAINT_NAME ` +
`WHERE columnUsages.TABLE_CATALOG = '${this.dbName}' AND tableConstraints.TABLE_CATALOG = '${this.dbName}'`;
const identityColumnsSql = `SELECT COLUMN_NAME, TABLE_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_CATALOG = '${this.dbName}' AND COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') = 1;`;
const indicesSql = `SELECT TABLE_NAME = t.name, INDEX_NAME = ind.name, IndexId = ind.index_id, ColumnId = ic.index_column_id, COLUMN_NAME = col.name, ind.*, ic.*, col.* ` +
const indicesSql = `SELECT TABLE_NAME = t.name, INDEX_NAME = ind.name, IndexId = ind.index_id, ColumnId = ic.index_column_id, COLUMN_NAME = col.name, IS_UNIQUE = ind.is_unique, ind.*, ic.*, col.* ` +
`FROM sys.indexes ind INNER JOIN sys.index_columns ic ON ind.object_id = ic.object_id and ind.index_id = ic.index_id INNER JOIN sys.columns col ON ic.object_id = col.object_id and ic.column_id = col.column_id ` +
`INNER JOIN sys.tables t ON ind.object_id = t.object_id WHERE ind.is_primary_key = 0 AND ind.is_unique = 0 AND ind.is_unique_constraint = 0 AND t.is_ms_shipped = 0 ORDER BY t.name, ind.name, ind.index_id, ic.index_column_id`;
`INNER JOIN sys.tables t ON ind.object_id = t.object_id WHERE ind.is_primary_key = 0 AND ind.is_unique_constraint = 0 AND t.is_ms_shipped = 0 ORDER BY t.name, ind.name, ind.index_id, ic.index_column_id`;
const [dbTables, dbColumns, dbConstraints, dbIdentityColumns, dbIndices]: ObjectLiteral[][] = await Promise.all([
this.query(tablesSql),
this.query(columnsSql),
@ -590,7 +590,8 @@ export class SqlServerQueryRunner implements QueryRunner {
.filter(dbIndex => dbIndex["TABLE_NAME"] === tableSchema.name && dbIndex["INDEX_NAME"] === dbIndexName)
.map(dbIndex => dbIndex["COLUMN_NAME"]);
return new IndexSchema(dbTable["TABLE_NAME"], dbIndexName, columnNames, false /* todo: uniqueness? */);
const isUnique = !!dbIndices.find(dbIndex => dbIndex["TABLE_NAME"] === tableSchema.name && dbIndex["INDEX_NAME"] === dbIndexName && dbIndex["IS_UNIQUE"] === true);
return new IndexSchema(dbTable["TABLE_NAME"], dbIndexName, columnNames, isUnique);
});
return tableSchema;

View File

@ -247,4 +247,29 @@ export interface EntitySchema { // todo: make it-to-date
};
};
/**
* Entity indices options.
*/
indices: {
[indexName: string]: {
/**
* Index column names.
*/
columns: string[];
/**
* Indicates if this index must be unique or not.
*/
unique: boolean;
/**
* If true, the index only references documents with the specified field.
* These indexes use less space but behave differently in some situations (particularly sorts).
* This option is only supported for mongodb database.
*/
sparse?: boolean;
};
};
}

View File

@ -2,6 +2,7 @@ import {EntitySchema} from "./EntitySchema";
import {MetadataArgsStorage} from "../metadata-args/MetadataArgsStorage";
import {TableMetadataArgs} from "../metadata-args/TableMetadataArgs";
import {ColumnMetadataArgs} from "../metadata-args/ColumnMetadataArgs";
import {IndexMetadataArgs} from "../metadata-args/IndexMetadataArgs";
import {RelationMetadataArgs} from "../metadata-args/RelationMetadataArgs";
import {JoinColumnMetadataArgs} from "../metadata-args/JoinColumnMetadataArgs";
import {JoinTableMetadataArgs} from "../metadata-args/JoinTableMetadataArgs";
@ -147,6 +148,22 @@ export class EntitySchemaTransformer {
}
});
}
// add relation metadata args from the schema
if (schema.indices) {
Object.keys(schema.indices).forEach(indexName => {
const indexSchema = schema.indices[indexName];
const indexAgrs: IndexMetadataArgs = {
target: schema.target || schema.name,
name: indexName,
unique: indexSchema.unique,
sparse: indexSchema.sparse,
columns: indexSchema.columns
};
metadataArgsStorage.indices.push(indexAgrs);
});
}
});
return metadataArgsStorage;

View File

@ -23,4 +23,10 @@ export interface IndexMetadataArgs {
*/
unique: boolean;
/**
* If true, the index only references documents with the specified field.
* These indexes use less space but behave differently in some situations (particularly sorts).
* This option is only supported for mongodb database.
*/
sparse?: boolean;
}

View File

@ -22,6 +22,13 @@ export class IndexMetadata {
*/
isUnique: boolean = false;
/**
* If true, the index only references documents with the specified field.
* These indexes use less space but behave differently in some situations (particularly sorts).
* This option is only supported for mongodb database.
*/
isSparse?: boolean;
/**
* Target class to which metadata is applied.
*/
@ -76,6 +83,7 @@ export class IndexMetadata {
if (options.args) {
this.target = options.args.target;
this.isUnique = options.args.unique;
this.isSparse = options.args.sparse;
this.givenName = options.args.name;
this.givenColumnNames = options.args.columns;
}

View File

@ -37,7 +37,7 @@ export class MongoSchemaBuilder implements SchemaBuilder {
const promises: Promise<any>[] = [];
this.connection.entityMetadatas.forEach(metadata => {
metadata.indices.forEach(index => {
const options = { name: index.name, unique: index.isUnique };
const options = { name: index.name, unique: index.isUnique, sparse: index.isSparse };
promises.push(queryRunner.createCollectionIndex(metadata.tableName, index.columnNamesWithOrderingMap, options));
});
});

View File

@ -357,13 +357,27 @@ export class RdbmsSchemaBuilder implements SchemaBuilder {
// drop all indices that exist in the table, but does not exist in the given composite indices
const dropQueries = tableSchema.indices
.filter(indexSchema => !metadata.indices.find(indexMetadata => indexMetadata.name === indexSchema.name))
.filter(indexSchema => {
const metadataIndex = metadata.indices.find(indexMetadata => indexMetadata.name === indexSchema.name);
if (!metadataIndex)
return true;
if (metadataIndex.isUnique !== indexSchema.isUnique)
return true;
if (metadataIndex.columns.length !== indexSchema.columnNames.length)
return true;
if (metadataIndex.columns.findIndex((col, i) => col.databaseName !== indexSchema.columnNames[i]) !== -1)
return true;
return false;
})
.map(async indexSchema => {
this.connection.logger.logSchemaBuild(`dropping an index: ${indexSchema.name}`);
tableSchema.removeIndex(indexSchema);
await this.queryRunner.dropIndex(metadata.tableName, indexSchema.name);
});
await Promise.all(dropQueries);
// then create table indices for all composite indices we have
const addQueries = metadata.indices
.filter(indexMetadata => !tableSchema.indices.find(indexSchema => indexSchema.name === indexMetadata.name))
@ -374,7 +388,7 @@ export class RdbmsSchemaBuilder implements SchemaBuilder {
await this.queryRunner.createIndex(indexSchema.tableName, indexSchema);
});
await Promise.all(dropQueries.concat(addQueries));
await Promise.all(addQueries);
});
}

View File

@ -0,0 +1,19 @@
import {Entity} from "../../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Column} from "../../../../../src/decorator/columns/Column";
import {Index} from "../../../../../src/decorator/Index";
@Entity()
@Index("IDX_TEST", ["firstname", "lastname"])
export class Person {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstname: string;
@Column()
lastname: string;
}

View File

@ -0,0 +1,90 @@
import "reflect-metadata";
import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils";
import {Connection} from "../../../../src/connection/Connection";
import {expect} from "chai";
import {EntityMetadata} from "../../../../src/metadata/EntityMetadata";
import {IndexMetadata} from "../../../../src/metadata/IndexMetadata";
import {Person} from "./entity/Person";
describe("indices > reading index from entity schema and updating database", () => {
let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [Person],
schemaCreate: true,
dropSchema: true
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));
describe("create index", function() {
it("should create a non unique index with 2 columns", () => Promise.all(connections.map(async connection => {
const queryRunner = connection.createQueryRunner();
const tableSchema = await queryRunner.loadTableSchema("person");
await queryRunner.release();
expect(tableSchema!.indices.length).to.be.equal(1);
expect(tableSchema!.indices[0].name).to.be.equal("IDX_TEST");
expect(tableSchema!.indices[0].isUnique).to.be.false;
expect(tableSchema!.indices[0].columnNames.length).to.be.equal(2);
expect(tableSchema!.indices[0].columnNames[0]).to.be.equal("firstname");
expect(tableSchema!.indices[0].columnNames[1]).to.be.equal("lastname");
})));
it("should update the index to be unique", () => Promise.all(connections.map(async connection => {
const entityMetadata = connection.entityMetadatas.find(x => x.name === "Person");
const indexMetadata = entityMetadata!.indices.find(x => x.name === "IDX_TEST");
indexMetadata!.isUnique = true;
await connection.synchronize(false);
const queryRunner = connection.createQueryRunner();
const tableSchema = await queryRunner.loadTableSchema("person");
await queryRunner.release();
expect(tableSchema!.indices.length).to.be.equal(1);
expect(tableSchema!.indices[0].name).to.be.equal("IDX_TEST");
expect(tableSchema!.indices[0].isUnique).to.be.true;
expect(tableSchema!.indices[0].columnNames.length).to.be.equal(2);
expect(tableSchema!.indices[0].columnNames[0]).to.be.equal("firstname");
expect(tableSchema!.indices[0].columnNames[1]).to.be.equal("lastname");
})));
it("should update the index swaping the 2 columns", () => Promise.all(connections.map(async connection => {
const entityMetadata = connection.entityMetadatas.find(x => x.name === "Person");
entityMetadata!.indices = [new IndexMetadata({
entityMetadata: <EntityMetadata>entityMetadata,
args: {
target: Person,
name: "IDX_TEST",
columns: ["lastname", "firstname"],
unique: false
}
})];
entityMetadata!.indices.forEach(index => index.build(connection.namingStrategy));
await connection.synchronize(false);
const queryRunner = connection.createQueryRunner();
const tableSchema = await queryRunner.loadTableSchema("person");
await queryRunner.release();
expect(tableSchema!.indices.length).to.be.equal(1);
expect(tableSchema!.indices[0].name).to.be.equal("IDX_TEST");
expect(tableSchema!.indices[0].isUnique).to.be.false;
expect(tableSchema!.indices[0].columnNames.length).to.be.equal(2);
expect(tableSchema!.indices[0].columnNames[0]).to.be.equal("lastname");
expect(tableSchema!.indices[0].columnNames[1]).to.be.equal("firstname");
})));
});
});

View File

@ -0,0 +1,29 @@
export const PersonSchema = {
name: "Person",
columns: {
Id: {
primary: true,
type: "int",
generated: "increment"
},
FirstName: {
type: String,
length: 30
},
LastName: {
type: String,
length: 50,
nullable: false
}
},
relations: {},
indices: {
IDX_TEST: {
unique: false,
columns: [
"FirstName",
"LastName"
]
}
}
};

View File

@ -0,0 +1,90 @@
import "reflect-metadata";
import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils";
import {Connection} from "../../../../src/connection/Connection";
import {EntityMetadata} from "../../../../src/metadata/EntityMetadata";
import {IndexMetadata} from "../../../../src/metadata/IndexMetadata";
import {expect} from "chai";
import {PersonSchema} from "./entity/Person";
describe("indices > reading index from entity schema and updating database", () => {
let connections: Connection[];
before(async () => connections = await createTestingConnections({
entitySchemas: [<any>PersonSchema],
schemaCreate: true,
dropSchema: true
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));
describe("create index", function() {
it("should create a non unique index with 2 columns", () => Promise.all(connections.map(async connection => {
const queryRunner = connection.createQueryRunner();
const tableSchema = await queryRunner.loadTableSchema("person");
await queryRunner.release();
expect(tableSchema!.indices.length).to.be.equal(1);
expect(tableSchema!.indices[0].name).to.be.equal("IDX_TEST");
expect(tableSchema!.indices[0].isUnique).to.be.false;
expect(tableSchema!.indices[0].columnNames.length).to.be.equal(2);
expect(tableSchema!.indices[0].columnNames[0]).to.be.equal("FirstName");
expect(tableSchema!.indices[0].columnNames[1]).to.be.equal("LastName");
})));
it("should update the index to be unique", () => Promise.all(connections.map(async connection => {
const entityMetadata = connection.entityMetadatas.find(x => x.name === "Person");
const indexMetadata = entityMetadata!.indices.find(x => x.name === "IDX_TEST");
indexMetadata!.isUnique = true;
await connection.synchronize(false);
const queryRunner = connection.createQueryRunner();
const tableSchema = await queryRunner.loadTableSchema("person");
await queryRunner.release();
expect(tableSchema!.indices.length).to.be.equal(1);
expect(tableSchema!.indices[0].name).to.be.equal("IDX_TEST");
expect(tableSchema!.indices[0].isUnique).to.be.true;
expect(tableSchema!.indices[0].columnNames.length).to.be.equal(2);
expect(tableSchema!.indices[0].columnNames[0]).to.be.equal("FirstName");
expect(tableSchema!.indices[0].columnNames[1]).to.be.equal("LastName");
})));
it("should update the index swaping the 2 columns", () => Promise.all(connections.map(async connection => {
const entityMetadata = connection.entityMetadatas.find(x => x.name === "Person");
entityMetadata!.indices = [new IndexMetadata({
entityMetadata: <EntityMetadata>entityMetadata,
args: {
target: entityMetadata!.target,
name: "IDX_TEST",
columns: ["LastName", "FirstName"],
unique: false
}
})];
entityMetadata!.indices.forEach(index => index.build(connection.namingStrategy));
await connection.synchronize(false);
const queryRunner = connection.createQueryRunner();
const tableSchema = await queryRunner.loadTableSchema("person");
await queryRunner.release();
expect(tableSchema!.indices.length).to.be.equal(1);
expect(tableSchema!.indices[0].name).to.be.equal("IDX_TEST");
expect(tableSchema!.indices[0].isUnique).to.be.false;
expect(tableSchema!.indices[0].columnNames.length).to.be.equal(2);
expect(tableSchema!.indices[0].columnNames[0]).to.be.equal("LastName");
expect(tableSchema!.indices[0].columnNames[1]).to.be.equal("FirstName");
})));
});
});

View File

@ -50,7 +50,7 @@ export interface TestingOptions {
/**
* Entity schemas needs to be included in the connection for the given test suite.
*/
entitySchemas?: EntitySchema[];
entitySchemas?: string[]|EntitySchema[];
/**
* Indicates if schema sync should be performed or not.
@ -141,8 +141,8 @@ export function setupTestingConnections(options?: TestingOptions): ConnectionOpt
entities: options && options.entities ? options.entities : [],
subscribers: options && options.subscribers ? options.subscribers : [],
entitySchemas: options && options.entitySchemas ? options.entitySchemas : [],
autoSchemaSync: options && options.entities ? options.schemaCreate : false,
dropSchema: options && options.entities ? options.dropSchema : false,
autoSchemaSync: options && (options.entities || options.entitySchemas) ? options.schemaCreate : false,
dropSchema: options && (options.entities || options.entitySchemas) ? options.dropSchema : false,
schema: options && options.schema ? options.schema : undefined,
});
});