import {Constraint, QuerySource} from '../types' import {AbstractBuilder} from '../builder/AbstractBuilder' import {AppClass} from '../../lifecycle/AppClass' import {ColumnBuilder, IndexBuilder, TableBuilder} from '../schema/TableBuilder' import {Collectable, Collection, Maybe} from '../../util' /** A scalar value which can be interpolated safely into an SQL query. */ export type ScalarEscapeValue = null | undefined | string | number | boolean | Date | QuerySafeValue; /** A list of scalar escape values. */ export type VectorEscapeValue = T[] | Collection /** All possible escaped query values. */ export type EscapeValue = T | VectorEscapeValue // FIXME | Select /** Object mapping string field names to EscapeValue items. */ export type EscapeValueObject = { [field: string]: EscapeValue } /** * A wrapper class whose value is safe to inject directly into a query. */ export class QuerySafeValue { constructor( /** The unescaped value. */ public readonly originalValue: unknown, /** The query-safe sanitized value. */ public readonly value: unknown, ) { } /** Cast the value to a query-safe string. */ toString(): string { return String(this.value) } } /** * Treat the value as raw SQL that can be injected directly into a query. * This is dangerous and should NEVER be used to wrap user input. * @param value */ export function raw(value: unknown): QuerySafeValue { return new QuerySafeValue(value, value) } /** * Abstract class defining a particular dialect of SQL that is used to render * query builders to strings of SQL of that dialect for execution by Connection * instances. */ export abstract class SQLDialect extends AppClass { /** * Escape the given value and return the query-safe equivalent. * @param value */ public abstract escape(value: EscapeValue): QuerySafeValue /** * Render a query source object as a qualified table name string ("tablename" as "alias"). * @param source */ public abstract renderQuerySource(source: QuerySource): string; /** * Render the given query builder as a "SELECT ..." query string. * * This function should escape the values before they are included in the query string. * @param builder */ public abstract renderSelect(builder: AbstractBuilder): string; /** * Render the given query builder as an "UPDATE ..." query string, setting the * column values from the given data object. * * This function should escape the values before they are included in the query string. * @param builder * @param data */ public abstract renderUpdate(builder: AbstractBuilder, data: {[key: string]: EscapeValue}): string; /** * Render the given query builder as an "UPDATE ..." query string, setting column values * for multiple distinct records based on their primary key. * @param builder * @param primaryKey * @param dataRows */ public abstract renderBatchUpdate(builder: AbstractBuilder, primaryKey: string, dataRows: Collectable<{[key: string]: EscapeValue}>): string; /** * Render the given query builder as a "DELETE ..." query string. * * This function should escape the values before they are included in the query string. * @param builder */ public abstract renderDelete(builder: AbstractBuilder): string; /** * Render the given query builder as a query that can be used to test if at * least 1 row exists for the given builder. * * The resultant query should return at least 1 row if that condition is met, * and should return NO rows otherwise. * * This function should escape the values before they are included in the query string. * * @example * The PostgreSQL dialect achieves this by removing the user-specified fields, * select-ing `TRUE`, and applying `LIMIT 1` to the query. This returns a single * row if the constraints have results, and nothing otherwise. * * @param builder */ public abstract renderExistential(builder: AbstractBuilder): string; /** * Render the given query as an "INSERT ..." query string, inserting rows for * the given data object(s). * * This function should escape the values before they are included in the query string. * * @param builder * @param data */ public abstract renderInsert(builder: AbstractBuilder, data: {[key: string]: EscapeValue}|{[key: string]: EscapeValue}[]): string; /** * Wrap the given query string as a "SELECT ..." query that returns the number of * rows matched by the original query string. * * The resultant query should return the `extollo_render_count` field with the * number of rows that the original `query` would return. * * @param query */ public abstract renderCount(query: string): string; /** * Given a rendered "SELECT ..." query string, wrap it such that the query will * only return the rows ranging from the `start` to `end` indices. * * @param query * @param start * @param end */ public abstract renderRangedSelect(query: string, start: number, end: number): string; /** * Given an array of Constraint objects, render them as WHERE-clause SQL in this dialect. * * This function should escape the values before they are included in the query string. * * @example * ```ts * dialect.renderConstraints([ * { * field: 'id', * operator: '<', * operand: 44, * preop: 'AND', * }, * { * field: 'id', * operator: '>', * operand: 30, * preop: 'AND', * }, * ]) // => 'id < 44 AND id > 30' * ``` * * @param constraints */ public abstract renderConstraints(constraints: Constraint[]): string; /** * Render the "SET ... [field = value ...]" portion of the update query. * * This function should escape the values before they are included in the query string. * * @example * ```ts * dialect.renderUpdateSet({field1: 'value', field2: 45}) * // => "SET field1 = 'value', field2 = 45" * ``` * * @param data */ public abstract renderUpdateSet(data: {[key: string]: EscapeValue}): string; /** * Given a table schema-builder, render a `CREATE TABLE...` query. * @param builder */ public abstract renderCreateTable(builder: TableBuilder): string; /** * Given a table schema-builder, render an `ALTER TABLE...` query. * @param builder */ public abstract renderAlterTable(builder: TableBuilder): Maybe; /** * Given a table schema-builder, render a `DROP TABLE...` query. * @param builder */ public abstract renderDropTable(builder: TableBuilder): string; /** * Render the table-column definitions for the table defined by * the given schema-builder. * * @example * ```ts * dialect.renderTableColumns(builder) * // => ['col1 varchar(100) NULL', 'col2 serial NOT NULL'] * ``` * * @param builder */ public abstract renderTableColumns(builder: TableBuilder): string[]; /** * Given an index schema-builder, render a `CREATE INDEX...` query. * @param builder */ public abstract renderCreateIndex(builder: IndexBuilder): string; /** * Given a column schema-builder, render an `ALTER TABLE... DROP COLUMN...` query. * @param builder */ public abstract renderDropColumn(builder: ColumnBuilder): string; /** * Given an index schema-builder, render a `DROP INDEX...` query. * @param builder */ public abstract renderDropIndex(builder: IndexBuilder): string; /** * Given an index schema-builder, render an `ALTER INDEX... RENAME...` query. * @param builder */ public abstract renderRenameIndex(builder: IndexBuilder): string; /** * Given an index schema-builder, render either an `ALTER INDEX...` query, * or a `DROP INDEX...; CREATE INDEX...` query. * @param builder */ public abstract renderRecreateIndex(builder: IndexBuilder): string; /** * Given a series of fully-formed queries, render them as a single transaction. * @example * ```ts * const queries = [ * 'SELECT * FROM a', * 'UPDATE b SET col = 123', * ] * * dialect.renderTransaction(queries) * // => 'BEGIN; SELECT * FROM a; UPDATE b SET col = 123; COMMIT;' * ``` * @param queries */ public abstract renderTransaction(queries: string[]): string; /** * Get the expression for the current timestamp as an escaped value. * @example `raw('NOW()')` */ public abstract currentTimestamp(): QuerySafeValue; /** * Given a table schema-builder, render a series of queries as a transaction * that apply the given schema to database. * @todo handle constraints better - ConstraintBuilder * @param builder */ public renderCommitSchemaTransaction(builder: TableBuilder): string { if ( builder.isDropping() || builder.isDroppingIfExists() ) { // If we're dropping the table, just return the DROP TABLE query return this.renderTransaction([ this.renderDropTable(builder), ]) } // Render the queries to create/update/drop indexes const indexes = Object.values(builder.getIndexes()) .filter(index => !index.isExisting() || index.isDirty()) .map(index => { if ( index.isDropping() || index.isDroppingIfExists() ) { return this.renderDropIndex(index) } if ( index.isExisting() ) { // The index was changed in the schema, but exists in the DB return this.renderRecreateIndex(index) } return this.renderCreateIndex(index) }) // Render the queries to rename indexes AFTER the above operations const renamedIndexes = Object.values(builder.getIndexes()) .filter(idx => idx.getRename()) .map(x => this.renderRenameIndex(x)) let parts: string[] = [] // Render the CREATE/ALTER TABLE query if ( !builder.isExisting() && builder.isDirty() ) { parts.push(this.renderCreateTable(builder)) } else if ( builder.isExisting() && builder.isDirty() ) { const alterTable = this.renderAlterTable(builder) if ( alterTable ) { parts.push(alterTable) } } // Render the various schema queries as a single transaction parts = parts.concat(...indexes) parts = parts.concat(...renamedIndexes) return this.renderTransaction(parts) } }