diff --git a/src/Database.ts b/src/Database.ts index 25fb474..c6ac1a2 100644 --- a/src/Database.ts +++ b/src/Database.ts @@ -12,7 +12,7 @@ export default class Database { } public async migrate (pool: Pool | PoolClient) { - return this.history?.migrate(pool); + return this.history?.migrate(this, pool); } public setHistory (initialiser: (history: History) => History) { diff --git a/src/History.ts b/src/History.ts index 1eed9c6..2e9404f 100644 --- a/src/History.ts +++ b/src/History.ts @@ -1,4 +1,5 @@ import { DatabaseError, Pool, PoolClient } from "pg"; +import Database from "./Database"; import { StackUtil } from "./IStrongPG"; import log, { color } from "./Log"; import Migration, { MigrationVersion } from "./Migration"; @@ -22,7 +23,11 @@ export class History { return this as any; } - public async migrate (pool: Pool | PoolClient) { + public async migrate (db: Database, pool: Pool | PoolClient) { + + for (const migration of this.migrations) + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + migration["db"] = db as Database; await pool.query(`CREATE TABLE IF NOT EXISTS migrations ( migration_index_start SMALLINT DEFAULT 0, diff --git a/src/IStrongPG.ts b/src/IStrongPG.ts index 8eb46b3..5382558 100644 --- a/src/IStrongPG.ts +++ b/src/IStrongPG.ts @@ -102,7 +102,10 @@ export namespace DataType { // INTERVAL, // string - export const CHAR: TypeStringMap[DataTypeID.CHAR] = "CHARACTER"; + export function CHAR (length?: number): TypeStringMap[DataTypeID.CHAR] { + return length === undefined ? "CHARACTER" + : `CHARACTER(${Math.round(length)})` as TypeStringMap[DataTypeID.CHAR]; + } export function VARCHAR (length?: number): TypeStringMap[DataTypeID.VARCHAR] { return length === undefined ? "CHARACTER VARYING" : `CHARACTER VARYING(${Math.round(length)})` as TypeStringMap[DataTypeID.VARCHAR]; diff --git a/src/Migration.ts b/src/Migration.ts index d723137..413e7d0 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -1,3 +1,4 @@ +import Database from "./Database"; import { StackUtil } from "./IStrongPG"; import { DatabaseSchema } from "./Schema"; import CreateCollation from "./statements/collation/CreateCollation"; @@ -9,6 +10,7 @@ import CreateOrReplaceFunction, { CreateOrReplaceFunctionInitialiser } from "./s import DropFunction from "./statements/function/DropFunction"; import CreateIndex, { CreateIndexInitialiser } from "./statements/index/CreateIndex"; import DropIndex from "./statements/index/DropIndex"; +import Statement from "./statements/Statement"; import AlterTable, { AlterTableInitialiser } from "./statements/table/AlterTable"; import CreateTable from "./statements/table/CreateTable"; import DropTable from "./statements/table/DropTable"; @@ -26,12 +28,20 @@ export default class Migration; + public constructor (schemaStart?: SCHEMA_START) { super(); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this.schemaStart = schemaStart as any; } + public then (statementSupplier: (db: Database) => Statement) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.add(() => statementSupplier(this.db)); + return this; + } + public createTable ( table: NAME, alter: NAME extends DatabaseSchema.TableName ? never : AlterTableInitialiser, diff --git a/src/Schema.ts b/src/Schema.ts index dffd6af..966be28 100644 --- a/src/Schema.ts +++ b/src/Schema.ts @@ -144,6 +144,16 @@ class Schema { return primaryKey[0]; } + public static getPrimaryKey (schema: SCHEMA) { + const primaryKey = schema["PRIMARY_KEY"] as Schema.Column[] | undefined; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + // const primaryKey = ?.[0]; + if (!primaryKey?.length) + throw new Error("No primary key"); + + return primaryKey; + } + public static isColumn (schema: SCHEMA, column: keyof SCHEMA, type: TypeString) { const columnType = schema[column] as TypeString; switch (type) { diff --git a/src/Table.ts b/src/Table.ts index 609b4ee..0556f55 100644 --- a/src/Table.ts +++ b/src/Table.ts @@ -3,6 +3,7 @@ import Schema, { TableSchema } from "./Schema"; import DeleteFromTable from "./statements/Delete"; import InsertIntoTable, { InsertIntoTableFactory } from "./statements/Insert"; import SelectFromTable from "./statements/Select"; +import TruncateTable from "./statements/Truncate"; import UpdateTable from "./statements/Update"; export default class Table { @@ -92,4 +93,8 @@ export default class Table { // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument return initialiser?.(query) ?? query; } + + public truncate () { + return new TruncateTable(this.name); + } } diff --git a/src/Transaction.ts b/src/Transaction.ts index 7dc4aa5..8801320 100644 --- a/src/Transaction.ts +++ b/src/Transaction.ts @@ -22,9 +22,9 @@ export default class Transaction { } } - protected readonly statements: Statement[] = []; + protected readonly statements: (Statement | (() => Statement))[] = []; - public add (statement: Statement) { + public add (statement: Statement | (() => Statement)) { this.statements.push(statement); return this; } @@ -37,7 +37,7 @@ export default class Transaction { } public compile () { - return this.statements.flatMap(statement => statement.compile()); + return this.statements.flatMap(statement => (typeof statement === "function" ? statement() : statement).compile()); } } diff --git a/src/statements/Insert.ts b/src/statements/Insert.ts index 5768092..14d45db 100644 --- a/src/statements/Insert.ts +++ b/src/statements/Insert.ts @@ -18,14 +18,14 @@ export interface InsertIntoTableConflictActionFactory[] = Schema.Column[], RESULT = []> extends Statement { public static columns[] = Schema.Column[]> (tableName: string, schema: SCHEMA, columns: COLUMNS, isUpsert = false): InsertIntoTableFactory { - const primaryKey = !isUpsert ? undefined : Schema.getSingleColumnPrimaryKey(schema); + const primaryKey = !isUpsert ? undefined : Schema.getPrimaryKey(schema); return { prepare: () => new InsertIntoTable(tableName, schema, columns, []), values: (...values: any[]) => { const query = new InsertIntoTable(tableName, schema, columns, columns.length && !values.length ? [] : [values] as never); if (isUpsert) { - query.onConflict(primaryKey!).doUpdate(update => { + query.onConflict(...primaryKey!).doUpdate(update => { for (let i = 0; i < columns.length; i++) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return, @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call update.set(columns[i], ((expr: any) => expr.var(`EXCLUDED.${String(columns[i])}`)) as never); diff --git a/src/statements/Select.ts b/src/statements/Select.ts index a848492..55ff54e 100644 --- a/src/statements/Select.ts +++ b/src/statements/Select.ts @@ -20,9 +20,14 @@ export default class SelectFromTable[number]>]>) { + public primaryKeyed (id: InputTypeFromString[number]>]>, initialiser?: ExpressionInitialiser, boolean>) { const primaryKey = Schema.getSingleColumnPrimaryKey(this.schema); - this.where(expr => expr.var(primaryKey).equals(id as never)); + this.where(expr => { + const e2 = expr.var(primaryKey).equals(id as never); + if (initialiser) + e2.and(initialiser); + return e2; + }); return this.limit(1); } diff --git a/src/statements/Statement.ts b/src/statements/Statement.ts index 716de93..91de82d 100644 --- a/src/statements/Statement.ts +++ b/src/statements/Statement.ts @@ -89,14 +89,19 @@ namespace Statement { public compile () { const operations: (string | Queryable)[] = this.compileStandaloneOperations(); - const parallelOperations = this.compileParallelOperations().join(","); + const parallelOperations = this.joinParallelOperations(this.compileParallelOperations()); if (parallelOperations) operations.unshift(parallelOperations); + return operations.flatMap(operation => this.queryable(this.compileOperation( typeof operation === "string" ? operation : operation.text), typeof operation === "string" ? undefined : operation.stack)); } + protected joinParallelOperations (operations: string[]) { + return operations.join(","); + } + protected compileParallelOperations (): string[] { return this.parallelOperations.flatMap(operation => operation.compile()).map(operation => operation.text); } diff --git a/src/statements/Truncate.ts b/src/statements/Truncate.ts new file mode 100644 index 0000000..a946284 --- /dev/null +++ b/src/statements/Truncate.ts @@ -0,0 +1,12 @@ +import Statement from "./Statement"; + +export default class TruncateTable extends Statement<[]> { + + public constructor (public readonly tableName: string | undefined) { + super(); + } + + public compile () { + return this.queryable(`TRUNCATE ${this.tableName ?? ""}`); + } +} diff --git a/src/statements/table/AlterTable.ts b/src/statements/table/AlterTable.ts index 64c8776..2fcc9f4 100644 --- a/src/statements/table/AlterTable.ts +++ b/src/statements/table/AlterTable.ts @@ -27,6 +27,10 @@ export default class AlterTable(AlterTableSubStatement.addColumn(name, type, initialiser)); } + public alterColumn (name: NAME, initialiser: Initialiser, AlterColumn>) { + return this.do<{ [KEY in NAME | keyof SCHEMA_END]: KEY extends NAME ? NEW_TYPE : SCHEMA_END[KEY & keyof SCHEMA_END] }>(AlterTableSubStatement.alterColumn(name, initialiser)); + } + public dropColumn (name: NAME) { return this.do>( AlterTableSubStatement.dropColumn(name)); @@ -79,7 +83,7 @@ class AlterTableSubStatement extends Statement { return new AlterTableSubStatement(`ADD COLUMN ${column} ${TypeString.resolve(type)}${columnStuffs}`); } - public static alterColumn (column: COLUMN, initialiser: Initialiser>) { + public static alterColumn (column: COLUMN, initialiser: Initialiser, AlterColumn>) { const statement = new AlterColumn(column); initialiser(statement); return statement; @@ -174,27 +178,32 @@ class CreateColumnSubStatement extends Statement { } } -export class AlterColumn extends Statement.Super { +export class AlterColumn extends Statement.Super> { public constructor (public name: NAME) { super(); } - // public reference?: ColumnReference; - - // public setReferences (reference: ColumnReference) { - // this.reference = reference; - // return this; - // } + public setType (type: TYPE, initialiser?: Initialiser>) { + return this.addStandaloneOperation>(AlterColumnSubStatement.setType(type, initialiser)); + } - public default (value: MigrationTypeFromString | ExpressionInitialiser<{}, MigrationTypeFromString>) { + public setDefault (value: MigrationTypeFromString | ExpressionInitialiser<{}, MigrationTypeFromString>) { return this.addStandaloneOperation(AlterColumnSubStatement.setDefault(value)); } - public notNull () { + public dropDefault () { + return this.addStandaloneOperation(AlterColumnSubStatement.dropDefault()); + } + + public setNotNull () { return this.addStandaloneOperation(AlterColumnSubStatement.setNotNull()); } + public dropNotNull () { + return this.addStandaloneOperation(AlterColumnSubStatement.dropNotNull()); + } + protected compileOperation (operation: string) { return `ALTER COLUMN ${this.name} ${operation}`; } @@ -210,10 +219,66 @@ class AlterColumnSubStatement extends Statement { return new AlterColumnSubStatement(`SET DEFAULT (${stringifiedValue})`, expr?.values); } + public static dropDefault () { + return new AlterColumnSubStatement("DROP DEFAULT"); + } + public static setNotNull () { return new AlterColumnSubStatement("SET NOT NULL"); } + public static dropNotNull () { + return new AlterColumnSubStatement("DROP NOT NULL"); + } + + public static setType (type: TYPE, initialiser?: Initialiser>) { + const setType = new AlterColumnSetType(type); + initialiser?.(setType); + return setType; + } + + private constructor (private readonly compiled: string, private readonly vars?: any[]) { + super(); + } + + public compile () { + return this.queryable(this.compiled, undefined, this.vars); + } +} + +export class AlterColumnSetType extends Statement.Super { + + public constructor (private readonly type: TYPE) { + super(); + this.addParallelOperation(AlterColumnSetTypeSubStatement.type(type)); + } + + public using () { // TODO accept params + return this.addParallelOperation(AlterColumnSetTypeSubStatement.using()); + } + + // TODO collate + + protected compileOperation (operation: string) { + return `${operation}`; + } + + protected override joinParallelOperations (operations: string[]): string { + return operations.join(" "); + } +} + +class AlterColumnSetTypeSubStatement extends Statement { + + public static type (type: TypeString) { + return new AlterColumnSetTypeSubStatement(`TYPE ${type}`); + } + + public static using () { + // TODO + return new AlterColumnSetTypeSubStatement("USING"); + } + private constructor (private readonly compiled: string, private readonly vars?: any[]) { super(); }