Import other modules into monorepo
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
270
src/orm/dialect/PostgreSQLDialect.ts
Normal file
270
src/orm/dialect/PostgreSQLDialect.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import {EscapeValue, QuerySafeValue, raw, SQLDialect} from './SQLDialect';
|
||||
import {Constraint, isConstraintGroup, isConstraintItem, SpecifiedField} from "../types";
|
||||
import {AbstractBuilder} from "../builder/AbstractBuilder";
|
||||
|
||||
/**
|
||||
* An implementation of the SQLDialect specific to PostgreSQL.
|
||||
*/
|
||||
export class PostgreSQLDialect extends SQLDialect {
|
||||
|
||||
public escape(value: EscapeValue): QuerySafeValue {
|
||||
if ( value instanceof QuerySafeValue ) return value
|
||||
else if ( Array.isArray(value) ) {
|
||||
return new QuerySafeValue(value, `(${value.map(v => this.escape(v)).join(',')})`)
|
||||
} else if ( String(value).toLowerCase() === 'true' || value === true ) {
|
||||
return new QuerySafeValue(value, 'TRUE')
|
||||
} else if ( String(value).toLowerCase() === 'false' || value === false ) {
|
||||
return new QuerySafeValue(value, 'FALSE')
|
||||
} else if ( typeof value === 'number' ) {
|
||||
return new QuerySafeValue(value, `${value}`)
|
||||
} else if ( value instanceof Date ) {
|
||||
const pad = (val: number) => val < 10 ? `0${val}` : `${val}`
|
||||
const [y, m, d, h, i, s] = [
|
||||
`${value.getFullYear()}`,
|
||||
`${pad(value.getMonth() + 1)}`,
|
||||
`${pad(value.getDate())}`,
|
||||
`${pad(value.getHours())}`,
|
||||
`${pad(value.getMinutes())}`,
|
||||
`${pad(value.getSeconds())}`,
|
||||
]
|
||||
|
||||
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 {
|
||||
const escaped = value.replace(/'/g, '\\\'') //.replace(/"/g, '\\"').replace(/`/g, '\\`')
|
||||
return new QuerySafeValue(value, `'${escaped}'`)
|
||||
}
|
||||
}
|
||||
|
||||
public renderCount(query: string): string {
|
||||
return [
|
||||
'SELECT COUNT(*) AS "extollo_render_count"',
|
||||
'FROM (',
|
||||
...query.split('\n').map(x => ` ${x}`),
|
||||
') AS extollo_target_query'
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
public renderRangedSelect(query: string, start: number, end: number): string {
|
||||
return [
|
||||
'SELECT *',
|
||||
'FROM (',
|
||||
...query.split('\n').map(x => ` ${x}`),
|
||||
') AS extollo_target_query',
|
||||
`OFFSET ${start} LIMIT ${(end - start) + 1}`
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/** Render the fields from the builder class to PostgreSQL syntax. */
|
||||
protected renderFields(builder: AbstractBuilder<any>) {
|
||||
return builder.appliedFields.map((field: SpecifiedField) => {
|
||||
let columnString: string
|
||||
if ( typeof field === 'string' ) columnString = field.split('.').map(x => `"${x}"`).join('.')
|
||||
else if ( field instanceof QuerySafeValue ) columnString = field.toString()
|
||||
else if ( typeof field.field === 'string' ) columnString = field.field.split('.').map(x => `"${x}"`).join('.')
|
||||
else columnString = field.field.toString()
|
||||
|
||||
let aliasString = ''
|
||||
if ( typeof field !== 'string' && !(field instanceof QuerySafeValue) ) aliasString = ` AS "${field.alias}"`
|
||||
|
||||
return `${columnString}${aliasString}`
|
||||
})
|
||||
}
|
||||
|
||||
public renderSelect(builder: AbstractBuilder<any>): string {
|
||||
const indent = (item: string, level = 1) => Array(level + 1).fill('').join(' ') + item
|
||||
const queryLines = [
|
||||
`SELECT${builder.appliedDistinction ? ' DISTINCT' : ''}`
|
||||
]
|
||||
|
||||
// Add fields
|
||||
// FIXME error if no fields
|
||||
const fields = this.renderFields(builder).map(x => indent(x)).join(',\n')
|
||||
|
||||
queryLines.push(fields)
|
||||
|
||||
// Add table source
|
||||
// FIXME error if no source
|
||||
const source = builder.querySource
|
||||
if ( source ) {
|
||||
const tableString = typeof source === 'string' ? source : source.table
|
||||
const table: string = tableString.split('.').map(x => `"${x}"`).join('.')
|
||||
queryLines.push('FROM ' + (typeof source === 'string' ? table : `${table} "${source.alias}"`))
|
||||
}
|
||||
|
||||
// Add constraints
|
||||
const wheres = this.renderConstraints(builder.appliedConstraints)
|
||||
if ( wheres.trim() ) {
|
||||
queryLines.push('WHERE')
|
||||
queryLines.push(wheres)
|
||||
}
|
||||
|
||||
// Add group by
|
||||
if ( builder.appliedGroupings?.length ) {
|
||||
const grouping = builder.appliedGroupings.map(group => {
|
||||
return indent(group.split('.').map(x => `"${x}"`).join('.'))
|
||||
}).join(',\n')
|
||||
|
||||
queryLines.push('GROUP BY')
|
||||
queryLines.push(grouping)
|
||||
}
|
||||
|
||||
// Add order by
|
||||
if ( builder.appliedOrder?.length ) {
|
||||
const ordering = builder.appliedOrder.map(x => indent(`${x.field.split('.').map(x => '"' + x + '"').join('.')} ${x.direction}`)).join(',\n')
|
||||
queryLines.push('ORDER BY')
|
||||
queryLines.push(ordering)
|
||||
}
|
||||
|
||||
// Add limit/offset
|
||||
const pagination = builder.appliedPagination
|
||||
if ( pagination.take ) {
|
||||
queryLines.push(`LIMIT ${pagination.take}${pagination.skip ? ' OFFSET ' + pagination.skip : ''}`)
|
||||
} else if ( pagination.skip ) {
|
||||
queryLines.push(`OFFSET ${pagination.skip}`)
|
||||
}
|
||||
|
||||
return queryLines.join('\n')
|
||||
}
|
||||
|
||||
// TODO support FROM, RETURNING
|
||||
public renderUpdate(builder: AbstractBuilder<any>, data: {[key: string]: EscapeValue}): string {
|
||||
const indent = (item: string, level = 1) => Array(level + 1).fill('').join(' ') + item
|
||||
const queryLines: string[] = []
|
||||
|
||||
// Add table source
|
||||
const source = builder.querySource
|
||||
if ( source ) {
|
||||
const tableString = typeof source === 'string' ? source : source.table
|
||||
const table: string = tableString.split('.').map(x => `"${x}"`).join('.')
|
||||
queryLines.push('UPDATE ' + (typeof source === 'string' ? table : `${table} "${source.alias}"`))
|
||||
}
|
||||
|
||||
queryLines.push(this.renderUpdateSet(data))
|
||||
|
||||
// Add constraints
|
||||
const wheres = this.renderConstraints(builder.appliedConstraints)
|
||||
if ( wheres.trim() ) {
|
||||
queryLines.push('WHERE')
|
||||
queryLines.push(wheres)
|
||||
}
|
||||
|
||||
return queryLines.join('\n')
|
||||
}
|
||||
|
||||
public renderExistential(builder: AbstractBuilder<any>): string {
|
||||
const query = builder.clone()
|
||||
.clearFields()
|
||||
.field(raw('TRUE'))
|
||||
.limit(1)
|
||||
|
||||
return this.renderSelect(query)
|
||||
}
|
||||
|
||||
// FIXME: subquery support here and with select
|
||||
public renderInsert(builder: AbstractBuilder<any>, data: {[key: string]: EscapeValue}|{[key: string]: EscapeValue}[] = []): string {
|
||||
const indent = (item: string, level = 1) => Array(level + 1).fill('').join(' ') + item
|
||||
const queryLines: string[] = []
|
||||
|
||||
if ( !Array.isArray(data) ) data = [data]
|
||||
const columns = Object.keys(data[0])
|
||||
|
||||
// Add table source
|
||||
const source = builder.querySource
|
||||
if ( source ) {
|
||||
const tableString = typeof source === 'string' ? source : source.table
|
||||
const table: string = tableString.split('.').map(x => `"${x}"`).join('.')
|
||||
queryLines.push('INSERT INTO ' + (typeof source === 'string' ? table : `${table} AS "${source.alias}"`)
|
||||
+ (columns.length ? ` (${columns.join(', ')})` : ''))
|
||||
}
|
||||
|
||||
if ( Array.isArray(data) && !data.length ) {
|
||||
queryLines.push('DEFAULT VALUES')
|
||||
} else {
|
||||
queryLines.push('VALUES')
|
||||
|
||||
const valueString = data.map(row => {
|
||||
const values = columns.map(x => this.escape(row[x]))
|
||||
return indent(`(${values.join(', ')})`)
|
||||
})
|
||||
.join(',\n')
|
||||
|
||||
queryLines.push(valueString)
|
||||
}
|
||||
|
||||
// Add return fields
|
||||
if ( builder.appliedFields?.length ) {
|
||||
queryLines.push('RETURNING')
|
||||
const fields = this.renderFields(builder).map(x => indent(x)).join(',\n')
|
||||
|
||||
queryLines.push(fields)
|
||||
}
|
||||
|
||||
return queryLines.join('\n')
|
||||
}
|
||||
|
||||
public renderDelete(builder: AbstractBuilder<any>): string {
|
||||
const indent = (item: string, level = 1) => Array(level + 1).fill('').join(' ') + item
|
||||
const queryLines: string[] = []
|
||||
|
||||
// Add table source
|
||||
const source = builder.querySource
|
||||
if ( source ) {
|
||||
const tableString = typeof source === 'string' ? source : source.table
|
||||
const table: string = tableString.split('.').map(x => `"${x}"`).join('.')
|
||||
queryLines.push('DELETE FROM ' + (typeof source === 'string' ? table : `${table} "${source.alias}"`))
|
||||
}
|
||||
|
||||
// Add constraints
|
||||
const wheres = this.renderConstraints(builder.appliedConstraints)
|
||||
if ( wheres.trim() ) {
|
||||
queryLines.push('WHERE')
|
||||
queryLines.push(wheres)
|
||||
}
|
||||
|
||||
// Add return fields
|
||||
if ( builder.appliedFields?.length ) {
|
||||
queryLines.push('RETURNING')
|
||||
|
||||
const fields = this.renderFields(builder).map(x => indent(x)).join(',\n')
|
||||
|
||||
queryLines.push(fields)
|
||||
}
|
||||
|
||||
return queryLines.join('\n')
|
||||
}
|
||||
|
||||
public renderConstraints(constraints: Constraint[]): string {
|
||||
const constraintsToSql = (constraints: Constraint[], level = 1): string => {
|
||||
const indent = Array(level * 2).fill(' ').join('')
|
||||
let statements = []
|
||||
|
||||
for ( const constraint of constraints ) {
|
||||
if ( isConstraintGroup(constraint) ) {
|
||||
statements.push(`${indent}${statements.length < 1 ? '' : constraint.preop + ' '}(\n${constraintsToSql(constraint.items, level + 1)}\n${indent})`)
|
||||
} else if ( isConstraintItem(constraint) ) {
|
||||
const field: string = constraint.field.split('.').map(x => `"${x}"`).join('.')
|
||||
statements.push(`${indent}${statements.length < 1 ? '' : constraint.preop + ' '}${field} ${constraint.operator} ${this.escape(constraint.operand).value}`)
|
||||
}
|
||||
}
|
||||
|
||||
return statements.filter(Boolean).join('\n')
|
||||
}
|
||||
|
||||
return constraintsToSql(constraints)
|
||||
}
|
||||
|
||||
public renderUpdateSet(data: {[key: string]: EscapeValue}) {
|
||||
const sets = []
|
||||
for ( const key in data ) {
|
||||
if ( !data.hasOwnProperty(key) ) continue
|
||||
|
||||
sets.push(` "${key}" = ${this.escape(data[key])}`)
|
||||
}
|
||||
|
||||
return ['SET', ...sets].join('\n')
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user