import { BaseQueryBuilder } from './base-query-builder' import type { OrderDirection, SelectColumn } from '../types' import type { Transaction } from '../core/transaction' import { WhereQueryBuilder } from './where-query-builder' import { use } from 'typescript-mix' import { HavingQueryBuilder } from './having-query-builder' export interface QueryBuilder extends BaseQueryBuilder, WhereQueryBuilder>, HavingQueryBuilder> {} export class QueryBuilder extends BaseQueryBuilder { @use(WhereQueryBuilder, HavingQueryBuilder) this: any private alreadyRemovedStar: boolean = false public selectColumns: string[] = ['*'] public fromTable: string = '' public fromAlias: string = '' public insertData: Record[] = [] public updateData: Record = {} public queryMode: 'select' | 'insert' | 'update' | 'delete' | 'upsert' = 'select' public upsertData: Record = {} public conflictColumns: string[] = [] /** * Sets the transaction context * @param {Transaction} trx - Transaction instance * @returns {QueryBuilder} Query builder instance */ useTransaction(trx: Transaction): QueryBuilder { this.setDriver(trx.getDriver()) return this } /** * Sets the table for all operations * @param {string} table - Table name * @param {string} [alias] - Optional table alias (for select queries) * @returns {QueryBuilder} Query builder instance */ public table(table: string, alias?: string): QueryBuilder { this.fromTable = table this.fromAlias = alias ? alias : '' return this } /** * Sets the table to query from (alias for table method) * @param {string} table - Table name * @param {string} [alias] - Optional table alias * @returns {QueryBuilder} Query builder instance */ public from(table: string, alias?: string): QueryBuilder { return this.table(table, alias) } /** * Sets the table to insert into (alias for table method) * @param {string} table - Table name * @returns {QueryBuilder} Query builder instance */ public into(table: string): QueryBuilder { return this.table(table) } /** * Initiates a SELECT query * @returns {QueryBuilder} Query builder instance */ public query(): QueryBuilder { this.queryMode = 'select' return this } /** * Sets the columns to select in the query * @param {SelectColumn | SelectColumn[]} [columns] - Columns to select (defaults to '*') * @returns {QueryBuilder} Query builder chain for method chaining */ public select(...columns: SelectColumn[]): QueryBuilder public select(column: SelectColumn): QueryBuilder public select(columns: SelectColumn[]): QueryBuilder public select(columns?: SelectColumn | SelectColumn[], ...rest: SelectColumn[]): QueryBuilder { this.queryMode = 'select' if (!columns) { this.selectColumns = ['*'] return this } // remove * from selectColumns but only one time if (!this.alreadyRemovedStar) { this.selectColumns = this.selectColumns.filter((col) => col !== '*') this.alreadyRemovedStar = true } if (Array.isArray(columns)) { const newColumns = columns.map((col) => { if (typeof col === 'string') { return col } else { // Handle object format like { alias: 'column' } const [alias, column] = Object.entries(col)[0] as [string, string] return `${this.sqlHelper.safeEscapeIdentifier(column)} AS ${this.sqlHelper.safeEscapeIdentifier(alias)}` } }) this.selectColumns.push(...newColumns) } else if (typeof columns === 'string') { this.selectColumns.push(columns) } else { // Handle object format const newColumns = Object.entries(columns).map( ([alias, column]) => `${this.sqlHelper.safeEscapeIdentifier(column)} AS ${this.sqlHelper.safeEscapeIdentifier(alias)}` ) this.selectColumns.push(...newColumns) } if (rest.length > 0) { this.select(rest) } return this } /** * Executes an UPSERT operation * @param {Record} data - Data to upsert * @returns {Promise} Upsert result */ public async upsert(data: Record): Promise { this.queryMode = 'upsert' this.upsertData = data const query = this.buildQuery() return await this.executeQuery(query.sql, query.params) } /** * Sets the conflict columns for upsert operations (ON CONFLICT) * @param {string | string[]} columns - Column(s) to check for conflicts * @returns {QueryBuilder} Query builder instance */ public onConflict(columns: string | string[]): QueryBuilder { this.conflictColumns = Array.isArray(columns) ? columns : [columns] return this } /** * Sets the data to insert and executes the INSERT operation * @param {Record | Record[]} data - Data to insert (single object or array of objects) * @returns {Promise} Inserted records (if returning is specified) */ public async insert(data: Record | Record[]): Promise { this.queryMode = 'insert' if (Array.isArray(data)) { this.insertData = data } else { this.insertData = [data] } const query = this.buildQuery() return await this.executeQuery(query.sql, query.params) } /** * Sets the data to update and executes the UPDATE operation * @param {Record} data - Data to update * @returns {Promise} Updated records (if returning is specified) */ public async update(data: Record): Promise { this.queryMode = 'update' this.updateData = data const query = this.buildQuery() return await this.executeQuery(query.sql, query.params) } /** * Executes a DELETE operation * @returns {Promise} Deleted records (if returning is specified) */ public async delete(): Promise { this.queryMode = 'delete' const query = this.buildQuery() return await this.executeQuery(query.sql, query.params) } /** * Adds an INNER JOIN to the query * @param {string} table - Table name to join * @param {string} on - Join condition * @param {string} [alias] - Optional table alias * @returns {QueryBuilder} Query builder chain for method chaining */ public join(table: string, on: string, alias?: string): QueryBuilder { this.addJoin('INNER', table, on, alias) return this } /** * Adds a LEFT JOIN to the query * @param {string} table - Table name to join * @param {string} on - Join condition * @param {string} [alias] - Optional table alias * @returns {QueryBuilder} Query builder chain for method chaining */ public leftJoin(table: string, on: string, alias?: string): QueryBuilder { this.addJoin('LEFT', table, on, alias) return this } /** * Adds a RIGHT JOIN to the query * @param {string} table - Table name to join * @param {string} on - Join condition * @param {string} [alias] - Optional table alias * @returns {QueryBuilder} Query builder chain for method chaining */ public rightJoin(table: string, on: string, alias?: string): QueryBuilder { this.addJoin('RIGHT', table, on, alias) return this } /** * Adds a FULL JOIN to the query * @param {string} table - Table name to join * @param {string} on - Join condition * @param {string} [alias] - Optional table alias * @returns {QueryBuilder} Query builder chain for method chaining */ public fullJoin(table: string, on: string, alias?: string): QueryBuilder { this.addJoin('FULL', table, on, alias) return this } /** * Adds an ORDER BY clause to the query * @param {string} column - Column name to order by * @param {OrderDirection} [direction='ASC'] - Sort direction * @returns {QueryBuilder} Query builder chain for method chaining */ public orderBy(column: string, direction: OrderDirection = 'ASC'): QueryBuilder { this.addOrderBy(column, direction) return this } /** * Adds a GROUP BY clause to the query * @param {string} column - Column name to group by * @returns {QueryBuilder} Query builder chain for method chaining */ public groupBy(column: string): QueryBuilder { this.addGroupBy(column) return this } /** * Adds a LIMIT clause to the query * @param {number} count - Number of rows to limit * @returns {QueryBuilder} Query builder chain for method chaining */ public limit(count: number): QueryBuilder { this.limitValue = count return this } /** * Adds an OFFSET clause to the query * @param {number} count - Number of rows to offset * @returns {QueryBuilder} Query builder chain for method chaining */ public offset(count: number): QueryBuilder { this.offsetValue = count return this } /** * Adds DISTINCT to the query * @returns {QueryBuilder} Query builder chain for method chaining */ public distinct(): QueryBuilder { this.distinctFlag = true return this } /** * Sets the columns to return after insert/update/delete * @param {string | string[]} [columns] - Columns to return (defaults to '*') * @returns {QueryBuilder} Query builder instance */ public returning(columns?: string | string[]): QueryBuilder { if (!columns) { this.returningColumns = ['*'] } else if (Array.isArray(columns)) { this.returningColumns = columns } else { this.returningColumns = [columns] } return this } /** * Executes an AVG query * @param {string} column - Column to average * @returns {Promise} Average result */ public async avg(column: string): Promise { const originalSelect = this.selectColumns this.selectColumns = [`AVG(${column}) as avg`] const query = this.buildQuery() const result = await this.executeQuery<{ avg: string | number }>(query.sql, query.params) // Restore original select this.selectColumns = originalSelect const avg = result[0]?.avg || 0 return typeof avg === 'string' ? parseFloat(avg) : avg } /** * Executes a SUM query * @param {string} column - Column to sum * @returns {Promise} Sum result */ public async sum(column: string): Promise { const originalSelect = this.selectColumns this.selectColumns = [`SUM(${column}) as sum`] const query = this.buildQuery() const result = await this.executeQuery<{ sum: string | number }>(query.sql, query.params) // Restore original select this.selectColumns = originalSelect const sum = result[0]?.sum || 0 return typeof sum === 'string' ? parseFloat(sum) : sum } /** * Executes a COUNT query * @param {string} [column='*'] - Column to count * @returns {Promise} Count result */ public async count(column: string = '*'): Promise { const originalSelect = this.selectColumns this.selectColumns = [`COUNT(${column}) as count`] const query = this.buildQuery() const result = await this.executeQuery<{ count: string | number }>(query.sql, query.params) // Restore original select this.selectColumns = originalSelect const count = result[0]?.count || 0 return typeof count === 'string' ? parseInt(count, 10) : count } /** * Executes the query and returns the first result * @returns {Promise} First result */ public async first(): Promise<(M & S) | null> public async first(): Promise<(T & S) | null> public async first(): Promise public async first(): Promise { const originalLimit = this.limitValue this.limitValue = 1 const query = this.buildQuery() const result = await this.executeQuery(query.sql, query.params) // Restore original limit this.limitValue = originalLimit return result[0] || null } /** * Executes the query and returns the results * @returns {Promise | Array | M[] | T[]>} Results */ public async get(): Promise> public async get(): Promise> public async get(): Promise public async get(): Promise { const query = this.buildQuery() return await this.executeQuery(query.sql, query.params) } /** * Returns the raw SQL query and parameters * @returns {{ sql: string; params: any[] }} SQL query and parameters */ public toSql(): { sql: string; params: any[] } { const query = this.buildQuery() return { sql: query.sql.replace(/\s+/g, ' '), params: query.params, } } /** * Returns the SQL query as a string * @returns {string} SQL query */ public toQuery(): string { const { sql, params } = this.buildQuery() return this.sqlHelper.toSql(sql, params) } /** * Builds the complete SQL query based on the current mode * @returns {{ sql: string; params: any[] }} SQL query and parameters * @throws {Error} When required parameters are missing */ private buildQuery(): { sql: string; params: any[] } { switch (this.queryMode) { case 'select': return this.driver.buildSelectQuery(this) case 'insert': return this.driver.buildInsertQuery(this) case 'update': return this.driver.buildUpdateQuery(this) case 'delete': return this.driver.buildDeleteQuery(this) case 'upsert': return this.driver.buildUpsertQuery(this) default: throw new Error('Invalid query mode') } } }