finish TreeModel implementation and updateMany builder method
This commit is contained in:
@@ -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 = []
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user