bigquery-client
Version:
A feature-rich Node.js client for Google BigQuery with support for CRUD operations, transactions, query building, and advanced features like aggregate functions, pagination, and logging.
635 lines (634 loc) • 24.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueryBuilder = void 0;
/**
* Class representing a SQL query builder for BigQuery.
*/
class QueryBuilder {
/**
* Creates an instance of QueryBuilder.
* @param table - The name of the table to query.
*/
constructor(table) {
this.columns = [];
this.values = [];
this.joins = [];
this.conditions = [];
this.setClauses = [];
this.groupByColumns = [];
this.orderByColumns = [];
this.limitValue = undefined;
this.offsetValue = undefined;
this.aggregateFunctions = [];
this.queryType = 'SELECT';
this.tableSamplePercentage = undefined;
this.isDistinct = false;
this.table = table;
}
/**
* Sets the columns to select in a SELECT query.
* @param columns - The columns to select. Defaults to ['*'].
* @returns The current instance of QueryBuilder.
*/
select(columns = ['*']) {
this.queryType = 'SELECT';
this.columns = columns;
return this;
}
/**
* Sets the columns to select in a SELECT query with DISTINCT.
* @param columns - The columns to select. Defaults to ['*'].
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no columns are provided.
*/
selectDistinct(columns = ['*']) {
this.isDistinct = true;
this.select(columns);
return this;
}
/**
* Sets the aggregate functions to select in a SELECT query.
* @param aggregates - An array of objects containing the aggregate function and the column to apply it on.
* @returns The current instance of QueryBuilder.
*/
selectAggregate(aggregates) {
this.queryType = 'SELECT';
this.aggregateFunctions = aggregates.map((a) => `${a.func.toUpperCase()}(${a.column})`);
return this;
}
/**
* Adds a CASE expression to the SELECT query.
* @param column - The column to check in the CASE expression.
* @param conditions - An array of objects containing `when` condition and `then` value.
* @param elseValue - The value to return if none of the conditions match.
* @param alias - The alias for the CASE result.
* @returns The current instance of QueryBuilder.
*/
case(column, conditions, elseValue, alias) {
let caseStatement = `CASE `;
for (const condition of conditions) {
caseStatement += `WHEN ${condition.when} THEN ? `;
this.values.push(condition.then);
}
if (elseValue !== undefined) {
caseStatement += `ELSE ? `;
this.values.push(elseValue);
}
caseStatement += `END ${alias ? `AS ${alias}` : ''}`;
this.columns.push(caseStatement);
return this;
}
/**
* Sets the rows to insert in an INSERT query.
* @param rows - An array of objects representing the rows to insert.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no rows are provided.
*/
insert(rows) {
if (rows.length === 0)
throw new Error("Insert requires at least one row.");
this.queryType = 'INSERT';
this.columns = Object.keys(rows[0]);
this.values = rows.flatMap(Object.values);
return this;
}
/**
* Sets the columns and values to update in an UPDATE query.
* @param set - An object representing the columns and values to update.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no columns are provided.
*/
update(set) {
if (Object.keys(set).length === 0)
throw new Error("Update requires at least one column to set.");
this.queryType = 'UPDATE';
this.setClauses = Object.keys(set).map((key) => `${key} = ?`);
this.values.push(...Object.values(set));
return this;
}
/**
* Sets the query type to DELETE.
* @returns The current instance of QueryBuilder.
*/
delete() {
this.queryType = 'DELETE';
return this;
}
/**
* Adds a JOIN clause to the query.
* @param table - The table to join.
* @param on - An object representing the join condition.
* @param type - The type of join. Defaults to 'INNER'.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no table or condition is provided.
*/
join(table, on, type = 'INNER') {
if (!table || Object.keys(on).length === 0)
throw new Error("Join requires a valid table and condition.");
// 🔹 Fix: Ensure correct aliasing
const onClause = Object.entries(on)
.map(([left, right]) => `${left} = ${right}`)
.join(' AND ');
this.joins.push(`${type} JOIN ${table} ON ${onClause}`);
return this;
}
/**
* Sets the columns for the GROUP BY clause.
* @param columns - The columns to group by.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no columns are provided.
*/
groupBy(columns) {
if (columns.length === 0)
throw new Error("GroupBy requires at least one column.");
this.groupByColumns = columns;
return this;
}
/**
* Sets the columns and directions for the ORDER BY clause.
* @param order - An array of objects representing the columns and directions to order by.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no columns are provided.
*/
orderBy(order) {
if (order.length === 0)
throw new Error("OrderBy requires at least one column.");
this.orderByColumns = order.map((o) => `${o.column} ${o.direction || 'ASC'}`);
return this;
}
/**
* Sets the LIMIT clause.
* @param limit - The maximum number of rows to return.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if the limit is negative.
*/
limit(limit) {
if (limit < 0)
throw new Error("Limit must be a positive number.");
this.limitValue = limit;
return this;
}
/**
* Sets the OFFSET clause.
* @param offset - The number of rows to skip.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if the offset is negative.
*/
offset(offset) {
if (offset < 0)
throw new Error("Offset must be a positive number.");
this.offsetValue = offset;
return this;
}
/**
* Adds a subquery to the SELECT clause.
* @param subquery - The subquery to add.
* @param alias - The alias for the subquery.
* @returns The current instance of QueryBuilder.
*/
selectSubquery(subquery, alias) {
this.queryType = 'SELECT';
this.columns.push(`(${subquery}) AS ${alias}`);
return this;
}
/**
* Adds a HAVING clause to the query.
* @param conditions - An object representing the conditions for the HAVING clause.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no conditions are provided.
*/
having(conditions) {
if (Object.keys(conditions).length === 0)
throw new Error("Having conditions cannot be empty.");
const havingConditions = Object.entries(conditions).map(([key, value]) => `${key} = ?`);
this.conditions.push(...havingConditions);
this.values.push(...Object.values(conditions));
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param value - The value to check for.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column or value is provided.
*/
whereLike(column, pattern) {
this.conditions.push(`${column} LIKE ?`);
this.values.push(pattern);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param pattern - The pattern to check for.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column or pattern is provided.
*/
whereNotLike(column, pattern) {
this.conditions.push(`${column} NOT LIKE ?`);
this.values.push(pattern);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param min - The minimum value.
* @param max - The maximum value.
* @returns The current instance of QueryBuilder.
*/
whereExists(subquery) {
this.conditions.push(`EXISTS (${subquery})`);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param min - The minimum value.
* @param max - The maximum value.
* @returns The current instance of QueryBuilder.
*/
whereNotExists(subquery) {
this.conditions.push(`NOT EXISTS (${subquery})`);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param min - The minimum value.
* @param max - The maximum value.
* @returns The current instance of QueryBuilder.
*/
whereBetween(column, min, max) {
this.conditions.push(`${column} BETWEEN ? AND ?`);
this.values.push(min, max);
return this;
}
/**
* Adds a UNION clause to the query.
* @param query - The query to union with.
* @param all - Whether to use UNION ALL.
* @returns The current instance of QueryBuilder.
*/
union(query, all = false) {
this.queryType = 'SELECT';
this.columns.push(`${all ? 'UNION ALL' : 'UNION'} ${query}`);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param conditions - An object representing the conditions for the WHERE clause.
* @returns The current instance of QueryBuilder.
*/
where(conditions) {
if (Object.keys(conditions).length === 0)
return this; // Allow empty conditions
this.conditions.push(...Object.keys(conditions).map((key) => `${key} = ?`));
this.values.push(...Object.values(conditions));
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param values - The values to check for.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column or values are provided.
*/
whereArray(column, values) {
if (values.length === 0)
throw new Error(`Array values for ${column} cannot be empty.`);
this.conditions.push(`${column} IN UNNEST(@${column})`);
this.values.push({ name: column, value: values });
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param value - The value to check for.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column or value is provided.
*/
wherePartition(column, value) {
this.conditions.push(`${column} = ?`);
this.values.push(value);
return this;
}
/**
* Adds a TABLESAMPLE clause to the query.
* @param percentage - The percentage of the table to sample.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if the percentage is not between 0 and 100.
*/
tableSample(percentage) {
if (percentage <= 0 || percentage > 100) {
throw new Error("Percentage must be between 0 and 100.");
}
this.tableSamplePercentage = percentage;
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param jsonPath - The JSON path to check.
* @param value - The value to check for.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column, jsonPath, or value is provided.
*/
whereJsonField(column, jsonPath, value) {
this.conditions.push(`JSON_EXTRACT(${column}, "$.${jsonPath}") = ?`);
this.values.push(value);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check for null.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column is provided.
*/
whereNull(column) {
this.conditions.push(`${column} IS NULL`);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check for null.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column is provided.
*/
whereNotNull(column) {
this.conditions.push(`${column} IS NOT NULL`);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param conditions - An object representing the conditions for the WHERE clause.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no conditions are provided.
*/
whereStructField(structColumn, field, value) {
this.conditions.push(`${structColumn}.${field} = ?`);
this.values.push(value);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param value - The value to check for.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column or value is provided.
*/
whereDateBetween(column, startDate, endDate) {
this.conditions.push(`${column} BETWEEN DATE(?) AND DATE(?)`);
this.values.push(startDate, endDate);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param value - The value to check for.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column or value is provided.
*/
whereExtract(column, part, value) {
this.conditions.push(`EXTRACT(${part} FROM ${column}) = ?`);
this.values.push(value);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param day - The day of the week to check for.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column or day is provided.
*/
whereDayOfWeek(column, day) {
this.conditions.push(`EXTRACT(DAYOFWEEK FROM ${column}) = ?`);
this.values.push(day);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param operator - The comparison operator.
* @param value - The value to compare against.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column, operator, or value is provided.
*/
whereDateComparison(column1, operator, column2) {
this.conditions.push(`${column1} ${operator} ${column2}`);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param format - The format to use for the comparison.
* @param value - The value to compare against.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column, format, or value is provided.
*/
selectFormattedDate(column, format, alias) {
const formattedColumn = `FORMAT_TIMESTAMP('${format}', ${column})`;
this.columns = this.columns.includes('*') ? [formattedColumn] : [...this.columns, formattedColumn];
if (alias) {
this.columns[this.columns.length - 1] += ` AS ${alias}`;
}
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param value - The value to compare against.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column or value is provided.
*/
whereCurrentDate(column) {
this.conditions.push(`${column} = CURRENT_DATE()`);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param value - The value to compare against.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column or value is provided.
*/
whereTimestampBetween(column, startDate, endDate) {
this.conditions.push(`${column} BETWEEN TIMESTAMP(?) AND TIMESTAMP(?)`);
this.values.push(startDate, endDate);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param value - The value to compare against.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column or value is provided.
*/
whereDateTrunc(column, part, alias) {
const truncatedColumn = `DATE_TRUNC(${column}, ${part})`;
if (alias) {
this.columns.push(`${truncatedColumn} AS ${alias}`);
}
else {
this.columns.push(truncatedColumn);
}
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param value - The value to compare against.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column or value is provided
*/
whereTimestampTrunc(column, part, alias) {
const truncatedColumn = `TIMESTAMP_TRUNC(${column}, ${part})`;
if (alias) {
this.columns.push(`${truncatedColumn} AS ${alias}`);
}
else {
this.columns.push(truncatedColumn);
}
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param value - The value to compare against.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column or value is provided.
*/
whereContains(column, searchTerm) {
this.conditions.push(`CONTAINS_SUBSTR(${column}, ?)`);
this.values.push(searchTerm);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param value - The value to compare against.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column or value is provided.
*/
whereArrayContains(column, value) {
this.conditions.push(`ARRAY_CONTAINS(?, ${column})`);
this.values.push(value);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param length - The length to compare against.
* @param operator - The comparison operator.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column, length, or operator is provided.
*/
whereArrayLength(column, length, operator) {
this.conditions.push(`ARRAY_LENGTH(${column}) ${operator} ?`);
this.values.push(length);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param value - The value to compare against.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column or value is provided.
*/
whereNotEqualNullSafe(column, value) {
this.conditions.push(`${column} IS DISTINCT FROM ?`);
this.values.push(value);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param values - The values to check against.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column or values are provided.
*/
selectStringAgg(column, separator, alias) {
const aggExpression = `STRING_AGG(${column}, '${separator}')`;
this.columns.push(alias ? `${aggExpression} AS ${alias}` : aggExpression);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param values - The values to check against.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column or values are provided.
*/
selectConditionalSum(column, condition, alias) {
const aggExpression = `SUM(CASE WHEN ${condition} THEN ${column} ELSE 0 END)`;
this.columns.push(alias ? `${aggExpression} AS ${alias}` : aggExpression);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param values - The values to check against.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column or values are provided.
*/
selectWindowFunction(func, partitionBy, orderBy, alias) {
const windowFunction = `${func}() OVER (PARTITION BY ${partitionBy} ORDER BY ${orderBy})`;
this.columns.push(alias ? `${windowFunction} AS ${alias}` : windowFunction);
return this;
}
/**
* Adds a WHERE clause to the query.
* @param column - The column to check.
* @param values - The values to check against.
* @returns The current instance of QueryBuilder.
* @throws Will throw an error if no column or values are provided.
*/
selectJsonField(column, jsonPath, alias) {
const jsonExtract = `JSON_VALUE(${column}, '$.${jsonPath}')`;
this.columns.push(alias ? `${jsonExtract} AS ${alias}` : jsonExtract);
return this;
}
/**
* Builds the SQL query string and returns it along with the parameters.
* @returns An object containing the query string and the parameters.
*/
build() {
let query = '';
switch (this.queryType) {
case 'SELECT':
const selectCols = this.aggregateFunctions.length ? this.aggregateFunctions : (this.columns.length ? this.columns : ['*']);
const distinctKeyword = this.isDistinct ? 'DISTINCT ' : '';
query = `SELECT ${distinctKeyword}${selectCols.join(', ')} FROM ${this.table}`;
if (this.joins.length) {
query += ` ${this.joins.join(' ')}`;
}
if (this.tableSamplePercentage !== undefined) {
query += ` TABLESAMPLE SYSTEM (${this.tableSamplePercentage})`;
}
break;
case 'INSERT':
const placeholders = Array(this.values.length / this.columns.length)
.fill(`(${this.columns.map(() => '?').join(', ')})`)
.join(', ');
query = `INSERT INTO ${this.table} (${this.columns.join(', ')}) VALUES ${placeholders}`;
break;
case 'UPDATE':
query = `UPDATE ${this.table} SET ${this.setClauses.join(', ')}`;
break;
case 'DELETE':
query = `DELETE FROM ${this.table}`;
break;
}
if (this.conditions.length)
query += ` WHERE ${this.conditions.join(' AND ')}`;
if (this.groupByColumns.length)
query += ` GROUP BY ${this.groupByColumns.join(', ')}`;
if (this.orderByColumns.length)
query += ` ORDER BY ${this.orderByColumns.join(', ')}`;
if (this.limitValue !== undefined)
query += ` LIMIT ${this.limitValue}`;
if (this.offsetValue !== undefined)
query += ` OFFSET ${this.offsetValue}`;
return { query, params: this.values };
}
}
exports.QueryBuilder = QueryBuilder;