270 lines
6.4 KiB
TypeScript
270 lines
6.4 KiB
TypeScript
import type { QueryResultRow } from 'pg';
|
|
import type { PostgreSQLClient } from './client';
|
|
import type { JoinCondition, OrderByCondition, QueryResult, WhereCondition } from './types';
|
|
|
|
/**
|
|
* PostgreSQL Query Builder
|
|
*
|
|
* Provides a fluent interface for building SQL queries
|
|
*/
|
|
export class PostgreSQLQueryBuilder {
|
|
private queryType: 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' | null = null;
|
|
private selectColumns: string[] = [];
|
|
private fromTable: string = '';
|
|
private joins: JoinCondition[] = [];
|
|
private whereConditions: WhereCondition[] = [];
|
|
private groupByColumns: string[] = [];
|
|
private havingConditions: WhereCondition[] = [];
|
|
private orderByConditions: OrderByCondition[] = [];
|
|
private limitCount: number | null = null;
|
|
private offsetCount: number | null = null;
|
|
private insertValues: Record<string, any> = {};
|
|
private updateValues: Record<string, any> = {};
|
|
|
|
private readonly client: PostgreSQLClient;
|
|
|
|
constructor(client: PostgreSQLClient) {
|
|
this.client = client;
|
|
}
|
|
|
|
/**
|
|
* SELECT statement
|
|
*/
|
|
select(columns: string | string[] = '*'): this {
|
|
this.queryType = 'SELECT';
|
|
this.selectColumns = Array.isArray(columns) ? columns : [columns];
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* FROM clause
|
|
*/
|
|
from(table: string): this {
|
|
this.fromTable = table;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* JOIN clause
|
|
*/
|
|
join(table: string, on: string, type: 'INNER' | 'LEFT' | 'RIGHT' | 'FULL' = 'INNER'): this {
|
|
this.joins.push({ type, table, on });
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* WHERE clause
|
|
*/
|
|
where(column: string, operator: string, value?: any): this {
|
|
this.whereConditions.push({ column, operator: operator as any, value });
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* GROUP BY clause
|
|
*/
|
|
groupBy(columns: string | string[]): this {
|
|
this.groupByColumns = Array.isArray(columns) ? columns : [columns];
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* ORDER BY clause
|
|
*/
|
|
orderBy(column: string, direction: 'ASC' | 'DESC' = 'ASC'): this {
|
|
this.orderByConditions.push({ column, direction });
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* LIMIT clause
|
|
*/
|
|
limit(count: number): this {
|
|
this.limitCount = count;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* OFFSET clause
|
|
*/
|
|
offset(count: number): this {
|
|
this.offsetCount = count;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* INSERT statement
|
|
*/
|
|
insert(table: string): this {
|
|
this.queryType = 'INSERT';
|
|
this.fromTable = table;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* VALUES for INSERT
|
|
*/
|
|
values(data: Record<string, any>): this {
|
|
this.insertValues = data;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* UPDATE statement
|
|
*/
|
|
update(table: string): this {
|
|
this.queryType = 'UPDATE';
|
|
this.fromTable = table;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* SET for UPDATE
|
|
*/
|
|
set(data: Record<string, any>): this {
|
|
this.updateValues = data;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* DELETE statement
|
|
*/
|
|
delete(table: string): this {
|
|
this.queryType = 'DELETE';
|
|
this.fromTable = table;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Build and execute the query
|
|
*/
|
|
async execute<T extends QueryResultRow = any>(): Promise<QueryResult<T>> {
|
|
const { sql, params } = this.build();
|
|
return await this.client.query<T>(sql, params);
|
|
}
|
|
|
|
/**
|
|
* Build the SQL query
|
|
*/
|
|
build(): { sql: string; params: any[] } {
|
|
const params: any[] = [];
|
|
let sql = '';
|
|
|
|
switch (this.queryType) {
|
|
case 'SELECT':
|
|
sql = this.buildSelectQuery(params);
|
|
break;
|
|
case 'INSERT':
|
|
sql = this.buildInsertQuery(params);
|
|
break;
|
|
case 'UPDATE':
|
|
sql = this.buildUpdateQuery(params);
|
|
break;
|
|
case 'DELETE':
|
|
sql = this.buildDeleteQuery(params);
|
|
break;
|
|
default:
|
|
throw new Error('Query type not specified');
|
|
}
|
|
|
|
return { sql, params };
|
|
}
|
|
|
|
private buildSelectQuery(params: any[]): string {
|
|
let sql = `SELECT ${this.selectColumns.join(', ')}`;
|
|
|
|
if (this.fromTable) {
|
|
sql += ` FROM ${this.fromTable}`;
|
|
}
|
|
|
|
// Add JOINs
|
|
for (const join of this.joins) {
|
|
sql += ` ${join.type} JOIN ${join.table} ON ${join.on}`;
|
|
}
|
|
|
|
// Add WHERE
|
|
if (this.whereConditions.length > 0) {
|
|
sql += ' WHERE ' + this.buildWhereClause(this.whereConditions, params);
|
|
}
|
|
|
|
// Add GROUP BY
|
|
if (this.groupByColumns.length > 0) {
|
|
sql += ` GROUP BY ${this.groupByColumns.join(', ')}`;
|
|
}
|
|
|
|
// Add HAVING
|
|
if (this.havingConditions.length > 0) {
|
|
sql += ' HAVING ' + this.buildWhereClause(this.havingConditions, params);
|
|
}
|
|
|
|
// Add ORDER BY
|
|
if (this.orderByConditions.length > 0) {
|
|
const orderBy = this.orderByConditions
|
|
.map(order => `${order.column} ${order.direction}`)
|
|
.join(', ');
|
|
sql += ` ORDER BY ${orderBy}`;
|
|
}
|
|
|
|
// Add LIMIT
|
|
if (this.limitCount !== null) {
|
|
sql += ` LIMIT $${params.length + 1}`;
|
|
params.push(this.limitCount);
|
|
}
|
|
|
|
// Add OFFSET
|
|
if (this.offsetCount !== null) {
|
|
sql += ` OFFSET $${params.length + 1}`;
|
|
params.push(this.offsetCount);
|
|
}
|
|
|
|
return sql;
|
|
}
|
|
|
|
private buildInsertQuery(params: any[]): string {
|
|
const columns = Object.keys(this.insertValues);
|
|
const placeholders = columns.map((_, i) => `$${params.length + i + 1}`);
|
|
|
|
params.push(...Object.values(this.insertValues));
|
|
|
|
return `INSERT INTO ${this.fromTable} (${columns.join(', ')}) VALUES (${placeholders.join(', ')})`;
|
|
}
|
|
|
|
private buildUpdateQuery(params: any[]): string {
|
|
const sets = Object.keys(this.updateValues).map((key, i) => {
|
|
return `${key} = $${params.length + i + 1}`;
|
|
});
|
|
|
|
params.push(...Object.values(this.updateValues));
|
|
|
|
let sql = `UPDATE ${this.fromTable} SET ${sets.join(', ')}`;
|
|
|
|
if (this.whereConditions.length > 0) {
|
|
sql += ' WHERE ' + this.buildWhereClause(this.whereConditions, params);
|
|
}
|
|
|
|
return sql;
|
|
}
|
|
|
|
private buildDeleteQuery(params: any[]): string {
|
|
let sql = `DELETE FROM ${this.fromTable}`;
|
|
|
|
if (this.whereConditions.length > 0) {
|
|
sql += ' WHERE ' + this.buildWhereClause(this.whereConditions, params);
|
|
}
|
|
|
|
return sql;
|
|
}
|
|
|
|
private buildWhereClause(conditions: WhereCondition[], params: any[]): string {
|
|
return conditions
|
|
.map(condition => {
|
|
if (condition.operator === 'IS NULL' || condition.operator === 'IS NOT NULL') {
|
|
return `${condition.column} ${condition.operator}`;
|
|
} else {
|
|
params.push(condition.value);
|
|
return `${condition.column} ${condition.operator} $${params.length}`;
|
|
}
|
|
})
|
|
.join(' AND ');
|
|
}
|
|
}
|