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) { 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): 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, 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): 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, 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): 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') } }