AsyncPipe; table schemata; migrations; File logging
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2021-07-25 09:15:01 -05:00
parent e86cf420df
commit fcce28081b
42 changed files with 3139 additions and 56 deletions

View File

@@ -0,0 +1,345 @@
import {Schema} from './Schema'
import {Awaitable, collect, Collection} from '../../util'
import {ConstraintType, TableBuilder} from './TableBuilder'
import {PostgresConnection} from '../connection/PostgresConnection'
import {Builder} from '../builder/Builder'
import {raw} from '../dialect/SQLDialect'
import {QueryRow} from '../types'
/**
* A PostgreSQL-compatible schema implementation.
*/
export class PostgresSchema extends Schema {
constructor(
connection: PostgresConnection,
public readonly schema: string = 'public',
) {
super(connection)
}
hasColumn(table: string, name: string): Awaitable<boolean> {
return (new Builder()).connection(this.connection)
.select(raw('*'))
.from('information_schema.columns')
.where('table_schema', '=', this.schema)
.where('table_name', '=', table)
.where('column_name', '=', name)
.exists()
}
async hasColumns(table: string, name: string[]): Promise<boolean> {
const num = await (new Builder()).connection(this.connection)
.select(raw('*'))
.from('information_schema.columns')
.where('table_schema', '=', this.schema)
.where('table_name', '=', table)
.whereIn('column_name', name)
.get()
.count()
return num === name.length
}
hasTable(name: string): Awaitable<boolean> {
return (new Builder()).connection(this.connection)
.select(raw('*'))
.from('information_schema.tables')
.where('table_schema', '=', this.schema)
.where('table_name', '=', name)
.exists()
}
async table(table: string): Promise<TableBuilder> {
return this.populateTable(new TableBuilder(table))
}
/**
* If the table for the given TableBuilder already exists in the
* database, fill in the columns, constraints, and indexes.
* @param table
* @protected
*/
protected async populateTable(table: TableBuilder): Promise<TableBuilder> {
if ( await this.hasTable(table.name) ) {
// Load the existing columns
const cols = await this.getColumns(table.name)
cols.each(col => {
table.column(col.column_name)
.type(col.data_type)
.pipe()
.when(col.is_nullable, builder => {
builder.isNullable()
return builder
})
.when(col.column_default, builder => {
builder.default(raw(col.column_default))
return builder
})
})
// Load the existing constraints
const constraints = await this.getConstraints(table.name)
// Apply the unique constraints
const uniques = constraints.where('constraint_type', '=', 'u')
.sortBy('constraint_name')
.groupBy('constraint_name')
for ( const key in uniques ) {
if ( !Object.prototype.hasOwnProperty.call(uniques, key) ) {
continue
}
table.constraint(key)
.type(ConstraintType.Unique)
.pipe()
.peek(constraint => {
collect<{column_name: string}>(uniques[key]) // eslint-disable-line camelcase
.pluck<string>('column_name')
.each(column => constraint.field(column))
})
.get()
.flagAsExistingInSchema()
}
// Apply the primary key constraints
constraints.where('constraint_type', '=', 'p')
.pipe()
.when(c => c.count() > 0, pk => {
pk.each(constraint => {
table.column(constraint.column_name)
.primary()
})
return pk
})
// Apply the non-null constraints
// Builder columns are non-null by default, so mark the others as nullable
const nonNullable = constraints.filter(x => !x.constraint_type)
.where('is_nullable', '=', 'NO')
collect<string>(Object.keys(table.getColumns()))
.map(column => {
return {
column,
}
})
.whereNotIn('column', nonNullable.pluck('column_name'))
.pluck<string>('column')
.each(column => {
table.column(column)
.nullable()
})
// Look up and apply the check constraints
const checkConstraints = await this.getCheckConstraints(table.name)
checkConstraints.each(constraint => {
table.constraint(constraint.constraint_name)
.type(ConstraintType.Check)
.expression(constraint.check_clause)
.flagAsExistingInSchema()
})
// Mark the columns as existing in the database
cols.each(col => {
table.column(col.column_name)
.flagAsExistingInSchema()
})
// Look up table indexes
const indexes = await this.getIndexes(table.name)
const groupedIndexes = indexes.groupBy('index_name')
for ( const key in groupedIndexes ) {
if ( !Object.prototype.hasOwnProperty.call(groupedIndexes, key) ) {
continue
}
table.index(key)
.pipe()
.peek(idx => {
collect<{column_name: string}>(groupedIndexes[key]) // eslint-disable-line camelcase
.pluck<string>('column_name')
.each(col => idx.field(col))
})
.when(groupedIndexes[key]?.[0]?.indisprimary, idx => {
idx.primary()
})
.when(groupedIndexes[key]?.[0]?.indisunique, idx => {
idx.unique()
})
.get()
.flagAsExistingInSchema()
}
table.flagAsExistingInSchema()
}
return table
}
/**
* Query the database to look up all indexes on a table, by column.
* @see https://stackoverflow.com/a/2213199/4971138
* @param table
* @protected
*/
protected async getIndexes(table: string): Promise<Collection<QueryRow>> {
const rawQuery = `
select
t.relname as table_name,
i.relname as index_name,
a.attname as column_name,
ix.*
from pg_class t
left join pg_attribute a
on a.attrelid = t.oid
left join pg_index ix
on t.oid = ix.indrelid
left join pg_class i
on i.oid = ix.indexrelid
left join pg_namespace n
on n.oid = i.relnamespace
where
a.attnum = any(ix.indkey)
and t.relkind = 'r'
and t.relname = '${table}'
and n.nspname = '${this.schema}'
order by
t.relname,
i.relname;
`
return (new Builder()).connection(this.connection)
.raw(rawQuery)
.get()
.collect()
}
/**
* Query the database to look up all constraints on a table, by column.
* @see https://dba.stackexchange.com/a/290854
* @param table
* @protected
*/
protected async getConstraints(table: string): Promise<Collection<QueryRow>> {
const rawQuery = `
SELECT * FROM (
SELECT
pgc.contype AS constraint_type,
pgc.conname AS constraint_name,
ccu.table_schema AS table_schema,
kcu.table_name AS table_name,
CASE WHEN (pgc.contype = 'f') THEN kcu.COLUMN_NAME ELSE ccu.COLUMN_NAME END AS column_name,
CASE WHEN (pgc.contype = 'f') THEN ccu.TABLE_NAME ELSE (null) END AS reference_table,
CASE WHEN (pgc.contype = 'f') THEN ccu.COLUMN_NAME ELSE (null) END AS reference_col,
CASE WHEN (pgc.contype = 'p') THEN 'yes' ELSE 'no' END AS auto_inc,
CASE WHEN (pgc.contype = 'p') THEN 'NO' ELSE 'YES' END AS is_nullable,
'integer' AS data_type,
'0' AS numeric_scale,
'32' AS numeric_precision
FROM
pg_constraint AS pgc
JOIN pg_namespace nsp
ON nsp.oid = pgc.connamespace
JOIN pg_class cls
ON pgc.conrelid = cls.oid
JOIN information_schema.key_column_usage kcu
ON kcu.constraint_name = pgc.conname
LEFT JOIN information_schema.constraint_column_usage ccu
ON pgc.conname = ccu.CONSTRAINT_NAME
AND nsp.nspname = ccu.CONSTRAINT_SCHEMA
WHERE
kcu.table_name = '${table}'
UNION
SELECT
NULL AS constraint_type,
NULL AS constraint_name,
table_schema,
table_name,
column_name,
NULL AS refrence_table,
NULL AS refrence_col,
'no' AS auto_inc,
is_nullable,
data_type,
numeric_scale,
numeric_precision
FROM information_schema.columns cols
WHERE
table_schema = '${this.schema}'
AND table_name = '${table}'
) AS child
ORDER BY table_name DESC
`
return (new Builder()).connection(this.connection)
.raw(rawQuery)
.get()
.collect()
}
/**
* @see https://dataedo.com/kb/query/postgresql/list-table-check-constraints
* @param table
* @protected
*/
protected async getCheckConstraints(table: string): Promise<Collection<QueryRow>> {
const rawQuery = `
SELECT
tc.table_schema,
tc.table_name,
ARRAY_AGG(col.column_name) AS columns,
tc.constraint_name,
cc.check_clause
FROM information_schema.table_constraints tc
JOIN information_schema.check_constraints cc
ON tc.constraint_schema = cc.constraint_schema
AND tc.constraint_name = cc.constraint_name
JOIN pg_namespace nsp
ON nsp.nspname = cc.constraint_schema
JOIN pg_constraint pgc
ON pgc.conname = cc.constraint_name
AND pgc.connamespace = nsp.oid
AND pgc.contype = 'c'
JOIN information_schema.columns col
ON col.table_schema = tc.table_schema
AND col.table_name = tc.table_name
AND col.ordinal_position = ANY(pgc.conkey)
WHERE
tc.constraint_schema NOT IN ('pg_catalog', 'information_schema')
AND tc.table_schema = '${this.schema}'
AND tc.table_name = '${table}'
GROUP BY
tc.table_schema,
tc.table_name,
tc.constraint_name,
cc.check_clause
ORDER BY
tc.table_schema,
tc.table_name
`
return (new Builder()).connection(this.connection)
.raw(rawQuery)
.get()
.collect()
}
/**
* Query the database to look up all columns on a table.
* @param table
* @protected
*/
protected async getColumns(table: string): Promise<Collection<QueryRow>> {
return (new Builder()).connection(this.connection)
.select(raw('*'))
.from('information_schema.columns')
.where('table_schema', '=', this.schema)
.where('table_name', '=', table)
.get()
.collect()
}
}

View File

@@ -1,14 +1,51 @@
import {Connection} from '../connection/Connection'
import {Awaitable} from '../../util'
import {TableBuilder} from './TableBuilder'
/**
* Represents a SQL-schema implementation.
*/
export abstract class Schema {
constructor(
/** The SQL connection to execute against. */
protected readonly connection: Connection,
) { }
/**
* Resolve true if the schema has a table with the given name.
* @param name
*/
public abstract hasTable(name: string): Awaitable<boolean>
/**
* Resolve true if the schema table with the given name has a column with the given name.
* @param table
* @param name
*/
public abstract hasColumn(table: string, name: string): Awaitable<boolean>
/**
* Resolve true if the schema table with the given name has all the specified columns.
* @param table
* @param name
*/
public abstract hasColumns(table: string, name: string[]): Awaitable<boolean>
/**
* Get a TableBuilder instance for a table on the schema.
* @param table
*/
public abstract table(table: string): Awaitable<TableBuilder>
/**
* Apply the table to the schema.
* @param schema
*/
public async commit(schema: TableBuilder): Promise<void> {
const query = this.connection
.dialect()
.renderCommitSchemaTransaction(schema)
await this.connection.query(query)
}
}

View File

@@ -1,109 +1,770 @@
import {Pipe} from '../../util'
import {collect, Maybe, ParameterizedCallback, Pipe} from '../../util'
import {FieldType} from '../types'
import {EscapeValue, QuerySafeValue, raw} from '../dialect/SQLDialect'
/**
* Base class with shared logic for the various schema
* builders (table, column, index).
*/
export abstract class SchemaBuilderBase {
/**
* Whether or not the schema item should be dropped.
* - `exists` - drop if exists
* @protected
*/
protected shouldDrop: 'yes'|'no'|'exists' = 'no'
/**
* The name the schema item should have if renaming.
* @protected
*/
protected shouldRenameTo?: string
/**
* If true, apply IF NOT EXISTS syntax.
* @protected
*/
protected shouldSkipIfExists = false
/** True if the schema has been modified since created/loaded. */
protected dirty = false
/** True if this resource exists, in some form, in the schema. */
protected existsInSchema = false
/** If the resource exists in the schema, the unaltered values it has. */
public originalFromSchema?: SchemaBuilderBase
constructor(
protected readonly name: string,
/** The name of the schema item. */
public readonly name: string,
) { }
/**
* Clone the properties of this resource to a different instance.
* @param newBuilder
*/
public cloneTo(newBuilder: SchemaBuilderBase): SchemaBuilderBase {
newBuilder.shouldDrop = this.shouldDrop
newBuilder.shouldRenameTo = this.shouldRenameTo
newBuilder.shouldSkipIfExists = this.shouldSkipIfExists
newBuilder.dirty = this.dirty
newBuilder.existsInSchema = this.existsInSchema
return newBuilder
}
/** True if this resource should be dropped. */
public isDropping(): boolean {
return this.shouldDrop === 'yes'
}
/** True if this resource should be dropped with IF EXISTS syntax. */
public isDroppingIfExists(): boolean {
return this.shouldDrop === 'exists'
}
/** True if this resource should be created with IF NOT EXISTS syntax. */
public isSkippedIfExisting(): boolean {
return this.shouldSkipIfExists
}
/** True if the resource already exists in some form in the schema. */
public isExisting(): boolean {
return this.existsInSchema
}
/** True if the resource has been modified since created/loaded. */
public isDirty(): boolean {
return this.dirty
}
/**
* Get the name this resource should be renamed to, if it exists.
*/
public getRename(): Maybe<string> {
return this.shouldRenameTo
}
/** Mark the resource to be removed. */
public drop(): this {
this.dirty = true
this.shouldDrop = 'yes'
return this
}
/** Mark the resource to be removed, if it exists. */
public dropIfExists(): this {
this.dirty = true
this.shouldDrop = 'exists'
return this
}
/**
* Rename the resource to a different name.
* @param to
*/
public rename(to: string): this {
this.dirty = true
this.shouldRenameTo = to
return this
}
/**
* Mark the resource to use IF NOT EXISTS syntax.
*/
public ifNotExists(): this {
this.shouldSkipIfExists = true
return this
}
/**
* Used internally.
* Mark that the resource exists in the schema in some form,
* and reset the `dirty` flag.
*/
public flagAsExistingInSchema(): this {
this.existsInSchema = true
this.dirty = false
this.originalFromSchema = this.cloneTo(this.cloneInstance())
return this
}
/** Get a Pipe containing this instance. */
pipe(): Pipe<this> {
return Pipe.wrap<this>(this)
}
/**
* Get a new instance of the concrete implementation of this class.
* @protected
*/
protected abstract cloneInstance(): this
}
/**
* Builder to specify the schema of a table column.
*/
export class ColumnBuilder extends SchemaBuilderBase {
/** The data type of the column. */
protected targetType?: FieldType
}
/** True if the column should allow NULL values. */
protected shouldBeNullable = false
export class IndexBuilder extends SchemaBuilderBase {
/** The default value of the column, if one should exist. */
protected defaultValue?: EscapeValue
protected fields: Set<string> = new Set<string>()
protected removedFields: Set<string> = new Set<string>()
protected shouldBeUnique = false
/** The data length of this column, if set */
protected targetLength?: number
/** True if this is a primary key constraint. */
protected shouldBePrimary = false
protected field(name: string): this {
/** True if this column should contain distinct values. */
protected shouldBeUnique = false
public originalFromSchema?: ColumnBuilder
constructor(
name: string,
/** The table this column belongs to. */
public readonly parent: TableBuilder,
) {
super(name)
}
public cloneTo(newBuilder: ColumnBuilder): ColumnBuilder {
super.cloneTo(newBuilder)
newBuilder.targetType = this.targetType
newBuilder.shouldBeNullable = this.shouldBeNullable
newBuilder.defaultValue = this.defaultValue
newBuilder.targetLength = this.targetLength
newBuilder.shouldBePrimary = this.shouldBePrimary
newBuilder.shouldBeUnique = this.shouldBeUnique
return newBuilder
}
/** Get the field type of the column, if it exists. */
public getType(): Maybe<FieldType> {
return this.targetType
}
/** Get the data-type length of the column, if it exists. */
public getLength(): Maybe<number> {
return this.targetLength
}
/** Get the default value of the column, if it exists. */
public getDefaultValue(): Maybe<EscapeValue> {
return this.defaultValue
}
/** True if the column should allow NULL values. */
public isNullable(): boolean {
return this.shouldBeNullable
}
/** True if the column is a primary key. */
public isPrimary(): boolean {
return this.shouldBePrimary
}
/** True if the column should require unique values. */
public isUnique(): boolean {
return this.shouldBeUnique
}
/**
* Specify the data type of the column.
* @param type
*/
public type(type: FieldType): this {
if ( this.targetType === type ) {
return this
}
this.dirty = true
this.targetType = type
return this
}
/**
* Make the column nullable.
*/
public nullable(): this {
if ( this.shouldBeNullable ) {
return this
}
this.dirty = true
this.shouldBeNullable = true
return this
}
/**
* Make the column non-nullable.
*/
public required(): this {
if ( !this.shouldBeNullable ) {
return this
}
this.dirty = true
this.shouldBeNullable = false
return this
}
/**
* Specify the default value of the column.
* @param value
*/
public default(value: EscapeValue): this {
if ( this.defaultValue === value ) {
return this
}
this.dirty = true
this.defaultValue = value
return this
}
/**
* Set the length of this column's data type.
* @param value
*/
public length(value: number): this {
if ( this.targetLength === value ) {
return this
}
this.dirty = true
this.targetLength = value
return this
}
/**
* Make this a primary-key column.
*/
primary(): this {
if ( this.shouldBePrimary ) {
return this
}
this.dirty = true
this.shouldBePrimary = true
return this
}
/**
* Make this column require distinct values.
*/
unique(): this {
if ( this.shouldBeUnique ) {
return this
}
this.dirty = true
this.shouldBeUnique = true
return this
}
protected cloneInstance(): this {
return new ColumnBuilder(this.name, this.parent) as this
}
}
/** Valid constraint types. */
export enum ConstraintType {
Unique = 'un',
Check = 'ck',
}
/**
* Builder to specify the schema of a table constraint.
*/
export class ConstraintBuilder extends SchemaBuilderBase {
public originalFromSchema?: ConstraintBuilder
/** The fields included in this constraint. */
protected fields: Set<string> = new Set<string>()
/** The type of this constraint. */
protected constraintType: ConstraintType = ConstraintType.Unique
/** The expression defining this constraint, if applicable. */
protected constraintExpression?: QuerySafeValue
constructor(
name: string,
/** The table this constraint belongs to. */
public readonly parent: TableBuilder,
) {
super(name)
}
/** Get the type of this constraint. */
public getType(): ConstraintType {
return this.constraintType
}
/** Get the fields included in this constraint. */
public getFields(): string[] {
return [...this.fields]
}
/** Get the expression used to evaluate this constraint, if it exists. */
public getExpression(): Maybe<QuerySafeValue> {
return this.constraintExpression
}
public cloneTo(newBuilder: ConstraintBuilder): ConstraintBuilder {
super.cloneTo(newBuilder)
newBuilder.fields = new Set<string>([...this.fields])
newBuilder.constraintType = this.constraintType
return newBuilder
}
protected cloneInstance(): this {
return new ConstraintBuilder(this.name, this.parent) as this
}
/** Add a field to this constraint. */
public field(name: string): this {
if ( this.fields.has(name) ) {
return this
}
this.dirty = true
this.fields.add(name)
return this
}
/** Remove a field from this constraint. */
public removeField(name: string): this {
if ( !this.fields.has(name) ) {
return this
}
this.dirty = true
this.fields.delete(name)
return this
}
/** Specify the type of this constraint. */
public type(type: ConstraintType): this {
if ( this.constraintType === type ) {
return this
}
this.dirty = true
this.constraintType = type
return this
}
/** Specify the expression used to evaluate this constraint, if applicable. */
public expression(sql: string | QuerySafeValue): this {
if ( String(this.constraintExpression) === String(sql) ) {
return this
}
this.dirty = true
if ( sql instanceof QuerySafeValue ) {
this.constraintExpression = sql
}
this.constraintExpression = raw(sql)
return this
}
}
/**
* Builder to specify the schema of a table index.
*/
export class IndexBuilder extends SchemaBuilderBase {
/** The fields included in the index. */
protected fields: Set<string> = new Set<string>()
/** Fields to remove from the index. */
protected removedFields: Set<string> = new Set<string>()
/** True if this is a unique index. */
protected shouldBeUnique = false
/** True if this is a primary key index. */
protected shouldBePrimary = false
public originalFromSchema?: IndexBuilder
constructor(
name: string,
/** The table this index belongs to. */
public readonly parent: TableBuilder,
) {
super(name)
}
public cloneTo(newBuilder: IndexBuilder): IndexBuilder {
super.cloneTo(newBuilder)
newBuilder.fields = new Set<string>([...this.fields])
newBuilder.removedFields = new Set<string>([...this.removedFields])
newBuilder.shouldBeUnique = this.shouldBeUnique
newBuilder.shouldBePrimary = this.shouldBePrimary
return newBuilder
}
/** Get the fields in this index. */
public getFields(): string[] {
return [...this.fields]
}
/** True if this index is a unique index. */
public isUnique(): boolean {
return this.shouldBeUnique
}
/** True if this index is the primary key index. */
public isPrimary(): boolean {
return this.shouldBePrimary
}
/**
* Add the given field to this index.
* @param name
*/
public field(name: string): this {
if ( this.fields.has(name) ) {
return this
}
this.dirty = true
this.fields.add(name)
return this
}
/**
* Remove the given field from this index.
* @param name
* @protected
*/
protected removeField(name: string): this {
if ( !this.fields.has(name) ) {
return this
}
this.dirty = true
this.removedFields.add(name)
this.fields.delete(name)
return this
}
/**
* Make this a primary-key index.
*/
primary(): this {
if ( this.shouldBePrimary ) {
return this
}
this.dirty = true
this.shouldBePrimary = true
return this
}
/**
* Make this a unique index.
*/
unique(): this {
if ( this.shouldBeUnique ) {
return this
}
this.dirty = true
this.shouldBeUnique = true
return this
}
protected cloneInstance(): this {
return new IndexBuilder(this.name, this.parent) as this
}
}
/**
* Builder to specify the schema of a table.
*/
export class TableBuilder extends SchemaBuilderBase {
/**
* Mapping of column name to column schemata.
* @protected
*/
protected columns: {[key: string]: ColumnBuilder} = {}
/**
* Mapping of index name to index schemata.
* @protected
*/
protected indexes: {[key: string]: IndexBuilder} = {}
/**
* Mapping of constraint name to constraint schemata.
* @protected
*/
protected constraints: {[key: string]: ConstraintBuilder} = {}
public originalFromSchema?: TableBuilder
public cloneTo(newBuilder: TableBuilder): TableBuilder {
super.cloneTo(newBuilder)
newBuilder.columns = {...this.columns}
newBuilder.indexes = {...this.indexes}
newBuilder.constraints = {...this.constraints}
return newBuilder
}
/**
* Get the columns defined on this table.
*/
public getColumns(): {[key: string]: ColumnBuilder} {
return {
...this.columns,
}
}
/**
* Get the indices defined on this table.
*/
public getIndexes(): {[key: string]: IndexBuilder} {
return {
...this.indexes,
}
}
/**
* Get the constraints defined on this table.
*/
public getConstraints(): {[key: string]: ConstraintBuilder} {
return {
...this.constraints,
}
}
/**
* Mark a column to be dropped.
* @param name
*/
public dropColumn(name: string): this {
this.dirty = true
this.column(name).drop()
return this
}
/**
* Mark a column to be renamed.
* @param from
* @param to
*/
public renameColumn(from: string, to: string): this {
this.column(from).rename(to)
return this
}
/**
* Mark an index to be dropped.
* @param name
*/
public dropIndex(name: string): this {
this.dirty = true
this.index(name).drop()
return this
}
/**
* Mark an index to be renamed.
* @param from
* @param to
*/
public renameIndex(from: string, to: string): this {
this.index(from).rename(to)
return this
}
public column(name: string) {
/**
* Add a column to this table.
* @param name
* @param callback
*/
public column(name: string, callback?: ParameterizedCallback<ColumnBuilder>): ColumnBuilder {
if ( !this.columns[name] ) {
this.columns[name] = new ColumnBuilder(name)
this.dirty = true
this.columns[name] = new ColumnBuilder(name, this)
}
if ( callback ) {
callback(this.columns[name])
}
return this.columns[name]
}
public index(name: string) {
/**
* Add an index to this table.
* @param name
* @param callback
*/
public index(name: string, callback?: ParameterizedCallback<IndexBuilder>): IndexBuilder {
if ( !this.indexes[name] ) {
this.indexes[name] = new IndexBuilder(name)
this.dirty = true
this.indexes[name] = new IndexBuilder(name, this)
}
if ( callback ) {
callback(this.indexes[name])
}
return this.indexes[name]
}
/**
* Add a constraint to this table.
* @param name
* @param callback
*/
public constraint(name: string, callback?: ParameterizedCallback<ConstraintBuilder>): ConstraintBuilder {
if ( !this.constraints[name] ) {
this.dirty = true
this.constraints[name] = new ConstraintBuilder(name, this)
}
if ( callback ) {
callback(this.constraints[name])
}
return this.constraints[name]
}
/**
* Generate a programmatically-incrementing constraint name.
* @param suffix
* @protected
*/
protected getNextAvailableConstraintName(suffix: ConstraintType): string {
let current = 1
let name = `${this.name}_${current}_${suffix}`
while ( this.constraints[name] ) {
current += 1
name = `${this.name}_${current}_${suffix}`
}
return name
}
/**
* Add a new check constraint with the given expression.
* @param expression
*/
public check(expression: string | QuerySafeValue): this {
const name = this.getNextAvailableConstraintName(ConstraintType.Check)
this.constraint(name)
.type(ConstraintType.Check)
.expression(expression)
return this
}
/**
* Add a new unique constraint for the given fields.
* @param fields
*/
public unique(...fields: string[]): this {
// First, check if an existing constraint exists with these fields
for ( const key in this.constraints ) {
if ( !Object.prototype.hasOwnProperty.call(this.constraints, key) ) {
continue
}
if ( this.constraints[key].getType() !== ConstraintType.Unique ) {
continue
}
const existingFields = collect<string>(this.constraints[key].getFields())
const intersection = existingFields.intersect(fields)
if ( existingFields.length === fields.length && intersection.length === fields.length ) {
return this
}
}
// If an existing constraint can't satisfy this, create a new one
const name = this.getNextAvailableConstraintName(ConstraintType.Unique)
this.constraint(name)
.type(ConstraintType.Unique)
.pipe()
.peek(constraint => {
fields.forEach(field => constraint.field(field))
})
return this
}
/**
* Add a primary key (column & index) to this table.
* @param name
* @param type
*/
public primaryKey(name: string, type: FieldType = FieldType.serial): ColumnBuilder {
this.dirty = true
return this.column(name)
.type(type)
.primary()
}
protected cloneInstance(): this {
return new TableBuilder(this.name) as this
}
}