You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lib/src/orm/dialect/SQLDialect.ts

302 lines
9.7 KiB

import {Constraint} from '../types'
import {AbstractBuilder} from '../builder/AbstractBuilder'
import {AppClass} from '../../lifecycle/AppClass'
import {ColumnBuilder, IndexBuilder, TableBuilder} from '../schema/TableBuilder'
/**
* A value which can be escaped to be interpolated into an SQL query.
*/
export type EscapeValue = null | undefined | string | number | boolean | Date | QuerySafeValue | EscapeValue[] // FIXME | Select<any>
/**
* Object mapping string field names to EscapeValue items.
*/
export type EscapeValueObject = { [field: string]: EscapeValue }
/**
* A wrapper class whose value is save 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 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 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): 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;
/**
* 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() ) {
parts.push(this.renderAlterTable(builder))
}
// Render the various schema queries as a single transaction
parts = parts.concat(...indexes)
parts = parts.concat(...renamedIndexes)
return this.renderTransaction(parts)
}
}