finish TreeModel implementation and updateMany builder method

This commit is contained in:
2022-09-12 12:36:33 -05:00
parent f63891ef99
commit c966904418
13 changed files with 580 additions and 44 deletions

View File

@@ -2,7 +2,7 @@ import {EscapeValue, QuerySafeValue, raw, SQLDialect} from './SQLDialect'
import {Constraint, inverseFieldType, isConstraintGroup, isConstraintItem, SpecifiedField} from '../types'
import {AbstractBuilder} from '../builder/AbstractBuilder'
import {ColumnBuilder, ConstraintBuilder, ConstraintType, IndexBuilder, TableBuilder} from '../schema/TableBuilder'
import {ErrorWithContext, Maybe} from '../../util'
import {collect, Collectable, Collection, ErrorWithContext, hasOwnProperty, Maybe} from '../../util'
/**
* An implementation of the SQLDialect specific to PostgreSQL.
@@ -14,7 +14,7 @@ export class PostgreSQLDialect extends SQLDialect {
public escape(value: EscapeValue): QuerySafeValue {
if ( value instanceof QuerySafeValue ) {
return value
} else if ( Array.isArray(value) ) {
} else if ( Array.isArray(value) || value instanceof Collection ) {
return new QuerySafeValue(value, `(${value.map(v => this.escape(v)).join(',')})`)
} else if ( String(value).toLowerCase() === 'true' || value === true ) {
return new QuerySafeValue(value, 'TRUE')
@@ -34,10 +34,10 @@ export class PostgreSQLDialect extends SQLDialect {
]
return new QuerySafeValue(value, `'${y}-${m}-${d} ${h}:${i}:${s}'`)
} else if ( !isNaN(Number(value)) ) {
return new QuerySafeValue(value, String(Number(value)))
} else if ( value === null || typeof value === 'undefined' ) {
return new QuerySafeValue(value, 'NULL')
} else if ( !isNaN(Number(value)) ) {
return new QuerySafeValue(value, String(Number(value)))
} else {
const escaped = value.replace(/'/g, '\'\'') // .replace(/"/g, '\\"').replace(/`/g, '\\`')
return new QuerySafeValue(value, `'${escaped}'`)
@@ -154,6 +154,78 @@ export class PostgreSQLDialect extends SQLDialect {
return queryLines.join('\n')
}
public renderBatchUpdate(builder: AbstractBuilder<any>, primaryKey: string, dataRows: Collectable<{[key: string]: EscapeValue}>): string {
const rows = Collection.normalize(dataRows)
const rawSql = builder.appliedRawSql
if ( rawSql ) {
return rawSql
}
const queryLines: string[] = []
// Add table source
const source = builder.querySource
let sourceAlias = 'extollo_update_source'
if ( !source ) {
throw new ErrorWithContext('No table specified for update query')
}
const tableString = typeof source === 'string' ? source : source.table
const table: string = tableString.split('.')
.map(x => `"${x}"`)
.join('.')
if ( typeof source !== 'string' && source.alias ) {
sourceAlias = source.alias
}
queryLines.push('UPDATE ' + (typeof source === 'string' ? table : `${table} "${source.alias}"`))
queryLines.push('SET')
const updateFields = this.getAllFieldsFromUpdateRows(rows)
const updateTuples = rows.map(row => {
return updateFields.map(field => {
if ( hasOwnProperty(row, field) ) {
return this.escape(row[field])
}
// FIXME: This is fairly inefficient. Probably a better way with a FROM ... SELECT
// return raw(`"${sourceAlias}"."${field}"`)
return raw(`(SELECT "${field}" FROM ${table} WHERE "${primaryKey}" = ${this.escape(row[primaryKey])})`)
})
})
queryLines.push(updateFields.map(field => ` "${field}" = "extollo_update_tuple"."${field}"`).join(',\n'))
queryLines.push('FROM (VALUES')
queryLines.push(
updateTuples.map(tuple => ` (${tuple.implode(', ')})`).join(',\n'),
)
queryLines.push(`) as extollo_update_tuple(${updateFields.map(x => `"${x}"`).join(', ')})`)
queryLines.push(`WHERE "extollo_update_tuple"."${primaryKey}" = "${sourceAlias}"."${primaryKey}" AND (`)
queryLines.push(this.renderConstraints(builder.appliedConstraints, 2))
queryLines.push(`)`)
return queryLines.join('\n')
}
private getAllFieldsFromUpdateRows(rows: Collection<{[key: string]: EscapeValue}>): Collection<string> {
return rows.reduce((fields: Collection<string>, row) => {
Object.keys(row).forEach(key => {
if ( !fields.includes(key) ) {
fields.push(key)
}
})
return fields
}, collect<string>())
}
// TODO support FROM, RETURNING
public renderUpdate(builder: AbstractBuilder<any>, data: {[key: string]: EscapeValue}): string {
const rawSql = builder.appliedRawSql
@@ -181,6 +253,14 @@ export class PostgreSQLDialect extends SQLDialect {
queryLines.push(wheres)
}
const fields = this.renderFields(builder).map(x => ` ${x}`)
.join(',\n')
if ( fields ) {
queryLines.push('RETURNING')
queryLines.push(fields)
}
return queryLines.join('\n')
}
@@ -298,8 +378,8 @@ export class PostgreSQLDialect extends SQLDialect {
return queryLines.join('\n')
}
public renderConstraints(allConstraints: Constraint[]): string {
const constraintsToSql = (constraints: Constraint[], level = 1): string => {
public renderConstraints(allConstraints: Constraint[], startingLevel = 1): string {
const constraintsToSql = (constraints: Constraint[], level = startingLevel): string => {
const indent = Array(level * 2).fill(' ')
.join('')
const statements = []

View File

@@ -2,15 +2,18 @@ import {Constraint} from '../types'
import {AbstractBuilder} from '../builder/AbstractBuilder'
import {AppClass} from '../../lifecycle/AppClass'
import {ColumnBuilder, IndexBuilder, TableBuilder} from '../schema/TableBuilder'
import {Collectable, Collection} from '../../util'
/**
* 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>
/** A scalar value which can be interpolated safely into an SQL query. */
export type ScalarEscapeValue = null | undefined | string | number | boolean | Date | QuerySafeValue;
/**
* Object mapping string field names to EscapeValue items.
*/
/** 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 }
/**
@@ -70,6 +73,15 @@ export abstract class SQLDialect extends AppClass {
*/
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.
*