Skip to content

Commit

Permalink
Add support for more statements, using misc statements in migrations,…
Browse files Browse the repository at this point in the history
… etc

- ALTER TABLE .. ALTER COLUMN .. TYPE
- TRUNCATE
- Composite primary key conflicts in upsert
- Fixed-length string data type
  • Loading branch information
ChiriVulpes committed Mar 26, 2024
1 parent ce3fdf5 commit 0da22af
Show file tree
Hide file tree
Showing 12 changed files with 141 additions and 21 deletions.
2 changes: 1 addition & 1 deletion src/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default class Database<SCHEMA extends DatabaseSchema> {
}

public async migrate (pool: Pool | PoolClient) {
return this.history?.migrate(pool);
return this.history?.migrate(this, pool);
}

public setHistory (initialiser: (history: History) => History<SCHEMA>) {
Expand Down
7 changes: 6 additions & 1 deletion src/History.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -22,7 +23,11 @@ export class History<SCHEMA extends DatabaseSchema | null = null> {
return this as any;
}

public async migrate (pool: Pool | PoolClient) {
public async migrate (db: Database<SCHEMA & DatabaseSchema>, pool: Pool | PoolClient) {

for (const migration of this.migrations)
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
migration["db"] = db as Database<any>;

await pool.query(`CREATE TABLE IF NOT EXISTS migrations (
migration_index_start SMALLINT DEFAULT 0,
Expand Down
5 changes: 4 additions & 1 deletion src/IStrongPG.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
10 changes: 10 additions & 0 deletions src/Migration.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Database from "./Database";
import { StackUtil } from "./IStrongPG";
import { DatabaseSchema } from "./Schema";
import CreateCollation from "./statements/collation/CreateCollation";
Expand All @@ -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";
Expand All @@ -26,12 +28,20 @@ export default class Migration<SCHEMA_START extends DatabaseSchema | null = null
private commits: MigrationCommit[] = [];
public readonly file = StackUtil.getCallerFile();

private db!: Database<SCHEMA_END>;

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<SCHEMA_END>) => Statement<any>) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
this.add(() => statementSupplier(this.db));
return this;
}

public createTable<NAME extends string, TABLE_SCHEMA_NEW> (
table: NAME,
alter: NAME extends DatabaseSchema.TableName<SCHEMA_END> ? never : AlterTableInitialiser<SCHEMA_END, null, TABLE_SCHEMA_NEW>,
Expand Down
10 changes: 10 additions & 0 deletions src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,16 @@ class Schema {
return primaryKey[0];
}

public static getPrimaryKey<SCHEMA extends TableSchema> (schema: SCHEMA) {
const primaryKey = schema["PRIMARY_KEY"] as Schema.Column<SCHEMA>[] | 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 extends TableSchema> (schema: SCHEMA, column: keyof SCHEMA, type: TypeString) {
const columnType = schema[column] as TypeString;
switch (type) {
Expand Down
5 changes: 5 additions & 0 deletions src/Table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SCHEMA extends TableSchema> {
Expand Down Expand Up @@ -92,4 +93,8 @@ export default class Table<SCHEMA extends TableSchema> {
// 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);
}
}
6 changes: 3 additions & 3 deletions src/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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());
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/statements/Insert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ export interface InsertIntoTableConflictActionFactory<SCHEMA extends TableSchema
export default class InsertIntoTable<SCHEMA extends TableSchema, COLUMNS extends Schema.Column<SCHEMA>[] = Schema.Column<SCHEMA>[], RESULT = []> extends Statement<RESULT> {

public static columns<SCHEMA extends TableSchema, COLUMNS extends Schema.Column<SCHEMA>[] = Schema.Column<SCHEMA>[]> (tableName: string, schema: SCHEMA, columns: COLUMNS, isUpsert = false): InsertIntoTableFactory<SCHEMA, COLUMNS> {
const primaryKey = !isUpsert ? undefined : Schema.getSingleColumnPrimaryKey(schema);
const primaryKey = !isUpsert ? undefined : Schema.getPrimaryKey(schema);

return {
prepare: () => new InsertIntoTable<SCHEMA, COLUMNS>(tableName, schema, columns, []),
values: (...values: any[]) => {
const query = new InsertIntoTable<SCHEMA, COLUMNS>(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);
Expand Down
9 changes: 7 additions & 2 deletions src/statements/Select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ export default class SelectFromTable<SCHEMA extends TableSchema, COLUMNS extends
return this;
}

public primaryKeyed (id: InputTypeFromString<SCHEMA[SingleStringUnion<Schema.PrimaryKey<SCHEMA>[number]>]>) {
public primaryKeyed (id: InputTypeFromString<SCHEMA[SingleStringUnion<Schema.PrimaryKey<SCHEMA>[number]>]>, initialiser?: ExpressionInitialiser<Schema.Columns<SCHEMA>, 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);
}

Expand Down
7 changes: 6 additions & 1 deletion src/statements/Statement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
12 changes: 12 additions & 0 deletions src/statements/Truncate.ts
Original file line number Diff line number Diff line change
@@ -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 ?? ""}`);
}
}
85 changes: 75 additions & 10 deletions src/statements/table/AlterTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export default class AlterTable<DB extends DatabaseSchema, SCHEMA_START = null,
return this.do<{ [KEY in NAME | keyof SCHEMA_END]: KEY extends NAME ? TYPE : SCHEMA_END[KEY & keyof SCHEMA_END] }>(AlterTableSubStatement.addColumn(name, type, initialiser));
}

public alterColumn<NAME extends keyof SCHEMA_END & string, NEW_TYPE extends TypeString> (name: NAME, initialiser: Initialiser<AlterColumn<NAME, SCHEMA_END[NAME] & TypeString>, AlterColumn<NAME, NEW_TYPE>>) {
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 extends SCHEMA_END extends null ? never : keyof SCHEMA_END & string> (name: NAME) {
return this.do<Omit<SCHEMA_END, NAME>>(
AlterTableSubStatement.dropColumn(name));
Expand Down Expand Up @@ -79,7 +83,7 @@ class AlterTableSubStatement extends Statement {
return new AlterTableSubStatement(`ADD COLUMN ${column} ${TypeString.resolve(type)}${columnStuffs}`);
}

public static alterColumn<COLUMN extends string, TYPE extends TypeString> (column: COLUMN, initialiser: Initialiser<AlterColumn<COLUMN, TYPE>>) {
public static alterColumn<COLUMN extends string, TYPE extends TypeString, NEW_TYPE extends TypeString> (column: COLUMN, initialiser: Initialiser<AlterColumn<COLUMN, TYPE>, AlterColumn<COLUMN, NEW_TYPE>>) {
const statement = new AlterColumn<COLUMN, TYPE>(column);
initialiser(statement);
return statement;
Expand Down Expand Up @@ -174,27 +178,32 @@ class CreateColumnSubStatement extends Statement {
}
}

export class AlterColumn<NAME extends string, TYPE extends TypeString> extends Statement.Super<AlterColumnSubStatement> {
export class AlterColumn<NAME extends string, TYPE extends TypeString> extends Statement.Super<AlterColumnSubStatement | AlterColumnSetType<TypeString>> {

public constructor (public name: NAME) {
super();
}

// public reference?: ColumnReference<TYPE>;

// public setReferences (reference: ColumnReference<TYPE>) {
// this.reference = reference;
// return this;
// }
public setType<TYPE extends TypeString> (type: TYPE, initialiser?: Initialiser<AlterColumnSetType<TYPE>>) {
return this.addStandaloneOperation<AlterColumn<NAME, TYPE>>(AlterColumnSubStatement.setType(type, initialiser));
}

public default (value: MigrationTypeFromString<TYPE> | ExpressionInitialiser<{}, MigrationTypeFromString<TYPE>>) {
public setDefault (value: MigrationTypeFromString<TYPE> | ExpressionInitialiser<{}, MigrationTypeFromString<TYPE>>) {
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}`;
}
Expand All @@ -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 extends TypeString> (type: TYPE, initialiser?: Initialiser<AlterColumnSetType<TYPE>>) {
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<TYPE extends TypeString> extends Statement.Super<AlterColumnSetTypeSubStatement> {

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();
}
Expand Down

0 comments on commit 0da22af

Please sign in to comment.