@algochad/prisma-core
Version:
A comprehensive NestJS library that provides EF-Core-like operations using Prisma and GraphQL. Features LINQ-style query builders, advanced data manipulation, GraphQL integration with genql, and a unified API for both Prisma and GraphQL operations. Includ
598 lines • 22.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AdvancedSqlQueryBuilder = void 0;
class AdvancedSqlQueryBuilder {
dialect;
schema;
parameterIndex = 0;
parameterMap = new Map();
options;
usedTables = new Set();
joinConditions = [];
queryPlan = null;
constructor(dialect, schema, options = {}) {
this.dialect = dialect;
this.schema = schema;
this.options = {
enableQueryOptimization: true,
enableIndexHints: true,
maxParameterCount: 1000,
useCompiledStatements: true,
enableQueryPlan: false,
...options,
};
}
buildSelectQuery(expression) {
this.reset();
if (this.options.enableQueryPlan) {
this.queryPlan = {
operation: 'SELECT',
tableScans: [],
indexUsage: {},
};
}
const parameters = [];
const selectClause = this.buildSelectClause(expression.select);
const fromClause = this.buildFromClause();
const joinClauses = this.buildJoinClauses(expression.include);
const whereClause = this.buildWhereClause(expression.where, parameters);
const groupByClause = this.buildGroupByClause(expression.groupBy);
const havingClause = this.buildHavingClause(expression.having, parameters);
const orderByClause = this.buildOrderByClause(expression.orderBy);
const limitClause = this.buildLimitClause(expression.take, expression.skip);
const sqlParts = [
`SELECT ${selectClause}`,
fromClause,
...joinClauses,
whereClause,
groupByClause,
havingClause,
orderByClause,
limitClause,
].filter((part) => part.trim() !== '');
let sql = sqlParts.join(' ');
if (this.options.enableQueryOptimization) {
sql = this.optimizeQuery(sql);
}
if (this.options.enableIndexHints) {
sql = this.addIndexHints(sql, expression);
}
return {
sql,
parameters,
estimatedCost: this.calculateQueryCost(),
usedIndexes: this.getUsedIndexes(),
};
}
buildCountQuery(expression) {
this.reset();
if (this.options.enableQueryPlan) {
this.queryPlan = {
operation: 'COUNT',
tableScans: [],
indexUsage: {},
};
}
const parameters = [];
const optimizedCountField = this.getOptimalCountField(expression.where);
const selectClause = `COUNT(${optimizedCountField})`;
const fromClause = this.buildFromClause();
const joinClauses = this.buildJoinClauses(expression.include);
const whereClause = this.buildWhereClause(expression.where, parameters);
const sqlParts = [
`SELECT ${selectClause}`,
fromClause,
...joinClauses,
whereClause,
].filter((part) => part.trim() !== '');
let sql = sqlParts.join(' ');
if (this.options.enableQueryOptimization) {
sql = this.optimizeCountQuery(sql, expression);
}
return {
sql,
parameters,
estimatedCost: this.calculateQueryCost(),
usedIndexes: this.getUsedIndexes(),
};
}
buildAggregationQuery(expression, aggregationType, columnName) {
this.reset();
if (this.options.enableQueryPlan) {
this.queryPlan = {
operation: 'AGGREGATE',
tableScans: [],
indexUsage: {},
};
}
const parameters = [];
this.validateAggregationColumn(columnName, aggregationType);
const columnInfo = this.schema.columns.get(columnName);
const quotedColumn = this.quoteIdentifier(columnName);
let selectClause;
switch (aggregationType) {
case 'MIN':
case 'MAX':
selectClause = `${aggregationType}(${quotedColumn}) as result`;
break;
case 'SUM':
if (this.dialect === 'postgresql' &&
columnInfo?.type === 'bigint') {
selectClause = `SUM(${quotedColumn}::numeric) as result`;
}
else {
selectClause = `SUM(${quotedColumn}) as result`;
}
break;
case 'AVG':
if (this.dialect === 'postgresql') {
selectClause = `AVG(${quotedColumn}::numeric) as result`;
}
else if (this.dialect === 'mysql') {
selectClause = `AVG(CAST(${quotedColumn} AS DECIMAL(65,30))) as result`;
}
else {
selectClause = `AVG(CAST(${quotedColumn} AS REAL)) as result`;
}
break;
}
const fromClause = this.buildFromClause();
const joinClauses = this.buildJoinClauses(expression.include);
const whereClause = this.buildWhereClause(expression.where, parameters);
const sqlParts = [
`SELECT ${selectClause}`,
fromClause,
...joinClauses,
whereClause,
].filter((part) => part.trim() !== '');
let sql = sqlParts.join(' ');
if (this.options.enableQueryOptimization) {
sql = this.optimizeAggregationQuery(sql, columnName, aggregationType);
}
return {
sql,
parameters,
estimatedCost: this.calculateQueryCost(),
usedIndexes: this.getUsedIndexes(),
};
}
buildWhereClause(where, parameters) {
if (!where || Object.keys(where).length === 0) {
return '';
}
const whereResult = this.buildCondition(where, parameters);
const whereSql = whereResult.sql;
if (!whereSql) {
return '';
}
this.analyzeWhereForIndexUsage(where);
return `WHERE ${whereSql}`;
}
buildCondition(condition, parameters) {
if (!condition || typeof condition !== 'object') {
return { sql: '', parameters };
}
const parts = [];
for (const [key, value] of Object.entries(condition)) {
if (key === 'AND') {
const andResult = this.buildLogicalOperator(value, 'AND', parameters);
if (andResult.sql) {
parts.push(`(${andResult.sql})`);
}
}
else if (key === 'OR') {
const orResult = this.buildLogicalOperator(value, 'OR', parameters);
if (orResult.sql) {
parts.push(`(${orResult.sql})`);
}
}
else if (key === 'NOT') {
const notResult = this.buildCondition(value, parameters);
if (notResult.sql) {
parts.push(`NOT (${notResult.sql})`);
}
}
else {
const fieldResult = this.buildFieldCondition(key, value, parameters);
if (fieldResult.sql) {
parts.push(fieldResult.sql);
}
}
}
return {
sql: parts.join(' AND '),
parameters,
};
}
buildFieldCondition(fieldName, condition, parameters) {
const quotedField = this.quoteIdentifier(fieldName);
const columnInfo = this.schema.columns.get(fieldName);
if (typeof condition !== 'object' || condition === null) {
parameters.push(this.normalizeValue(condition, columnInfo));
return {
sql: `${quotedField} = ${this.getParameterPlaceholder()}`,
parameters,
};
}
const parts = [];
for (const [operator, value] of Object.entries(condition)) {
let sql = '';
switch (operator) {
case 'equals':
parameters.push(this.normalizeValue(value, columnInfo));
sql = `${quotedField} = ${this.getParameterPlaceholder()}`;
break;
case 'not':
if (typeof value === 'object' && value !== null) {
const nestedResult = this.buildFieldCondition(fieldName, value, parameters);
sql = `NOT (${nestedResult.sql})`;
}
else {
parameters.push(this.normalizeValue(value, columnInfo));
sql = `${quotedField} != ${this.getParameterPlaceholder()}`;
}
break;
case 'in':
sql = this.buildInCondition(quotedField, Array.isArray(value) ? value : [value], parameters, columnInfo);
break;
case 'notIn':
sql = this.buildInCondition(quotedField, Array.isArray(value) ? value : [value], parameters, columnInfo, true);
break;
case 'lt':
parameters.push(this.normalizeValue(value, columnInfo));
sql = `${quotedField} < ${this.getParameterPlaceholder()}`;
break;
case 'lte':
parameters.push(this.normalizeValue(value, columnInfo));
sql = `${quotedField} <= ${this.getParameterPlaceholder()}`;
break;
case 'gt':
parameters.push(this.normalizeValue(value, columnInfo));
sql = `${quotedField} > ${this.getParameterPlaceholder()}`;
break;
case 'gte':
parameters.push(this.normalizeValue(value, columnInfo));
sql = `${quotedField} >= ${this.getParameterPlaceholder()}`;
break;
case 'contains':
sql = this.buildStringOperation(quotedField, 'CONTAINS', String(value ?? ''), parameters, columnInfo);
break;
case 'startsWith':
sql = this.buildStringOperation(quotedField, 'STARTS_WITH', String(value ?? ''), parameters, columnInfo);
break;
case 'endsWith':
sql = this.buildStringOperation(quotedField, 'ENDS_WITH', String(value ?? ''), parameters, columnInfo);
break;
case 'mode':
continue;
case 'search':
sql = this.buildFullTextSearch(quotedField, String(value ?? ''), parameters);
break;
case 'isEmpty':
sql = this.buildEmptyCheck(quotedField, columnInfo, Boolean(value));
break;
case 'isSet':
sql = value
? `${quotedField} IS NOT NULL`
: `${quotedField} IS NULL`;
break;
case 'every':
case 'some':
case 'none':
sql = this.buildArrayCondition(quotedField, operator, value, parameters, columnInfo);
break;
default:
console.warn(`Unknown operator: ${operator} for field ${fieldName}`);
break;
}
if (sql) {
parts.push(sql);
}
}
return {
sql: parts.join(' AND '),
parameters,
};
}
buildInCondition(quotedField, values, parameters, columnInfo, isNotIn = false) {
if (!Array.isArray(values) || values.length === 0) {
return isNotIn ? 'TRUE' : 'FALSE';
}
if (values.length > 100 && this.dialect === 'postgresql') {
parameters.push(values.map((v) => this.normalizeValue(v, columnInfo)));
const operator = isNotIn ? 'NOT = ANY' : '= ANY';
return `${quotedField} ${operator}(${this.getParameterPlaceholder()}::${this.getArrayType(columnInfo)}[])`;
}
const normalizedValues = values.map((v) => {
parameters.push(this.normalizeValue(v, columnInfo));
return this.getParameterPlaceholder();
});
const operator = isNotIn ? 'NOT IN' : 'IN';
return `${quotedField} ${operator} (${normalizedValues.join(', ')})`;
}
buildStringOperation(quotedField, operation, value, parameters, columnInfo) {
let pattern;
let operator;
switch (operation) {
case 'CONTAINS':
pattern = `%${value}%`;
break;
case 'STARTS_WITH':
pattern = `${value}%`;
break;
case 'ENDS_WITH':
pattern = `%${value}`;
break;
}
if (this.dialect === 'postgresql') {
operator = 'ILIKE';
}
else {
operator = 'LIKE';
}
parameters.push(pattern);
return `${quotedField} ${operator} ${this.getParameterPlaceholder()}`;
}
buildFullTextSearch(quotedField, searchTerm, parameters) {
switch (this.dialect) {
case 'postgresql':
parameters.push(searchTerm);
return `to_tsvector(${quotedField}) @@ plainto_tsquery(${this.getParameterPlaceholder()})`;
case 'mysql':
parameters.push(searchTerm);
return `MATCH(${quotedField}) AGAINST(${this.getParameterPlaceholder()} IN NATURAL LANGUAGE MODE)`;
case 'sqlite':
parameters.push(`%${searchTerm}%`);
return `${quotedField} LIKE ${this.getParameterPlaceholder()}`;
default:
parameters.push(`%${searchTerm}%`);
return `${quotedField} LIKE ${this.getParameterPlaceholder()}`;
}
}
buildArrayCondition(quotedField, operator, condition, parameters, columnInfo) {
if (this.dialect === 'postgresql' && columnInfo?.isArray) {
switch (operator) {
case 'every':
return `NOT EXISTS (SELECT 1 FROM unnest(${quotedField}) AS elem WHERE NOT (${this.buildArrayElementCondition('elem', condition, parameters)}))`;
case 'some':
return `EXISTS (SELECT 1 FROM unnest(${quotedField}) AS elem WHERE ${this.buildArrayElementCondition('elem', condition, parameters)})`;
case 'none':
return `NOT EXISTS (SELECT 1 FROM unnest(${quotedField}) AS elem WHERE ${this.buildArrayElementCondition('elem', condition, parameters)})`;
}
}
else {
switch (operator) {
case 'some':
return this.buildJsonArraySome(quotedField, condition, parameters);
default:
console.warn(`Array operator ${operator} not fully supported for JSON arrays`);
return 'TRUE';
}
}
}
buildSelectClause(select) {
if (!select || Object.keys(select).length === 0) {
return '*';
}
const selectedColumns = [];
for (const [columnName, isSelected] of Object.entries(select)) {
if (isSelected === true) {
const quotedColumn = this.quoteIdentifier(columnName);
selectedColumns.push(quotedColumn);
}
else if (typeof isSelected === 'object' && isSelected !== null) {
const quotedColumn = this.quoteIdentifier(columnName);
selectedColumns.push(quotedColumn);
}
}
return selectedColumns.length > 0 ? selectedColumns.join(', ') : '*';
}
buildFromClause() {
const quotedTable = this.quoteIdentifier(this.schema.tableName);
this.usedTables.add(this.schema.tableName);
return `FROM ${quotedTable}`;
}
buildJoinClauses(include) {
if (!include || Object.keys(include).length === 0) {
return [];
}
const joinClauses = [];
for (const [relationName, includeConfig] of Object.entries(include)) {
const relation = this.schema.relations.get(relationName);
if (relation) {
const joinClause = this.buildJoinClause(relation, includeConfig);
if (joinClause) {
joinClauses.push(joinClause);
}
}
}
return joinClauses;
}
buildJoinClause(relation, includeConfig) {
return '';
}
buildOrderByClause(orderBy) {
if (!orderBy || orderBy.length === 0) {
return '';
}
const orderParts = [];
for (const order of orderBy) {
if (typeof order === 'object' && order !== null) {
for (const [fieldName, direction] of Object.entries(order)) {
const quotedField = this.quoteIdentifier(fieldName);
const dir = direction === 'desc' ? 'DESC' : 'ASC';
orderParts.push(`${quotedField} ${dir}`);
}
}
}
return orderParts.length > 0 ? `ORDER BY ${orderParts.join(', ')}` : '';
}
buildLimitClause(take, skip) {
const parts = [];
if (take !== undefined) {
if (this.dialect === 'sqlite' || this.dialect === 'postgresql') {
parts.push(`LIMIT ${take}`);
if (skip !== undefined && skip > 0) {
parts.push(`OFFSET ${skip}`);
}
}
else if (this.dialect === 'mysql') {
if (skip !== undefined && skip > 0) {
parts.push(`LIMIT ${skip}, ${take}`);
}
else {
parts.push(`LIMIT ${take}`);
}
}
}
else if (skip !== undefined && skip > 0) {
if (this.dialect === 'postgresql') {
parts.push(`OFFSET ${skip}`);
}
}
return parts.join(' ');
}
getParameterPlaceholder() {
this.parameterIndex++;
switch (this.dialect) {
case 'postgresql':
return `$${this.parameterIndex}`;
case 'mysql':
case 'sqlite':
return '?';
default:
return '?';
}
}
quoteIdentifier(identifier) {
switch (this.dialect) {
case 'postgresql':
case 'sqlite':
return `"${identifier}"`;
case 'mysql':
return `\`${identifier}\``;
default:
return `"${identifier}"`;
}
}
normalizeValue(value, columnInfo) {
if (value === null || value === undefined) {
return null;
}
if (!columnInfo) {
return value;
}
switch (columnInfo.type) {
case 'date':
return value instanceof Date ? value : new Date(value);
case 'boolean':
return Boolean(value);
case 'number':
return Number(value);
case 'json':
return typeof value === 'string'
? value
: JSON.stringify(value);
default:
return value;
}
}
reset() {
this.parameterIndex = 0;
this.parameterMap.clear();
this.usedTables.clear();
this.joinConditions = [];
this.queryPlan = null;
}
buildLogicalOperator(conditions, operator, parameters) {
const parts = [];
for (const condition of conditions) {
const result = this.buildCondition(condition, parameters);
if (result.sql) {
parts.push(result.sql);
}
}
return {
sql: parts.length > 0 ? parts.join(` ${operator} `) : '',
parameters,
};
}
buildGroupByClause(groupBy) {
return '';
}
buildHavingClause(having, parameters) {
return '';
}
getOptimalCountField(where) {
return '*';
}
validateAggregationColumn(columnName, aggregationType) {
const column = this.schema.columns.get(columnName);
if (!column) {
throw new Error(`Column '${columnName}' not found in table '${this.schema.tableName}'`);
}
if ((aggregationType === 'SUM' || aggregationType === 'AVG') &&
!['number', 'decimal', 'bigint'].includes(column.type)) {
throw new Error(`Column '${columnName}' of type '${column.type}' cannot be used with ${aggregationType}`);
}
}
optimizeQuery(sql) {
return sql;
}
optimizeCountQuery(sql, expression) {
return sql;
}
optimizeAggregationQuery(sql, columnName, aggregationType) {
return sql;
}
addIndexHints(sql, expression) {
return sql;
}
analyzeWhereForIndexUsage(where) {
}
calculateQueryCost() {
return 1;
}
getUsedIndexes() {
return [];
}
getArrayType(columnInfo) {
if (!columnInfo)
return 'text';
switch (columnInfo.type) {
case 'number':
return 'integer';
case 'string':
return 'text';
case 'boolean':
return 'boolean';
default:
return 'text';
}
}
buildArrayElementCondition(elementAlias, condition, parameters) {
return 'TRUE';
}
buildJsonArraySome(quotedField, condition, parameters) {
return 'TRUE';
}
buildEmptyCheck(quotedField, columnInfo, isEmpty = true) {
const operator = isEmpty ? '=' : '!=';
if (columnInfo?.isArray) {
if (this.dialect === 'postgresql') {
return `array_length(${quotedField}, 1) ${operator === '=' ? 'IS NULL' : 'IS NOT NULL'}`;
}
else {
return `json_array_length(${quotedField}) ${operator} 0`;
}
}
else if (columnInfo?.type === 'string') {
return `(${quotedField} ${operator} '' OR ${quotedField} IS ${isEmpty ? '' : 'NOT '}NULL)`;
}
else {
return `${quotedField} IS ${isEmpty ? '' : 'NOT '}NULL`;
}
}
}
exports.AdvancedSqlQueryBuilder = AdvancedSqlQueryBuilder;
//# sourceMappingURL=advanced-sql-builder.js.map