|
|
|
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 extends ScalarEscapeValue> = T[] | Collection<T>
|
|
|
|
|
|
|
|
/** All possible escaped query values. */
|
|
|
|
export type EscapeValue<T extends ScalarEscapeValue = ScalarEscapeValue> = T | VectorEscapeValue<T> // FIXME | Select<any>
|
|
|
|
|
|
|
|
/** 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<any>): 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<any>, 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<any>, 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<any>): 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<any>): 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<any>, 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<string>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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)
|
|
|
|
}
|
|
|
|
}
|