UNPKG

@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
"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