UNPKG

drizzle-cube

Version:

Drizzle ORM-first semantic layer with Cube.js compatibility. Type-safe analytics and dashboards with SQL injection protection.

1,539 lines 419 kB
var XT = Object.defineProperty; var bT = (T, E, R) => E in T ? XT(T, E, { enumerable: !0, configurable: !0, writable: !0, value: R }) : T[E] = R; var f = (T, E, R) => bT(T, typeof E != "symbol" ? E + "" : E, R); import { sql as n, count as SE, sum as y, max as uE, min as FE, and as d, countDistinct as KT, SQL as yT, or as $T, gt as fE, lt as WE, gte as Q, lte as Z, isNull as gT, isNotNull as JT, notInArray as xT, ne as wT, inArray as XE, eq as mE, desc as vT, asc as bE } from "drizzle-orm"; class pE { /** * Helper method to build pattern for string matching * Can be overridden by specific adapters if needed */ buildPattern(E, R) { switch (E) { case "contains": case "notContains": return `%${R}%`; case "startsWith": return `${R}%`; case "endsWith": return `%${R}`; default: return R; } } } class QT extends pE { getEngineType() { return "postgres"; } /** * Build PostgreSQL time dimension using DATE_TRUNC function * Extracted from executor.ts:649-670 and multi-cube-builder.ts:306-320 */ buildTimeDimension(E, R) { switch (E) { case "year": return n`DATE_TRUNC('year', ${R}::timestamp)`; case "quarter": return n`DATE_TRUNC('quarter', ${R}::timestamp)`; case "month": return n`DATE_TRUNC('month', ${R}::timestamp)`; case "week": return n`DATE_TRUNC('week', ${R}::timestamp)`; case "day": return n`DATE_TRUNC('day', ${R}::timestamp)::timestamp`; case "hour": return n`DATE_TRUNC('hour', ${R}::timestamp)`; case "minute": return n`DATE_TRUNC('minute', ${R}::timestamp)`; case "second": return n`DATE_TRUNC('second', ${R}::timestamp)`; default: return R; } } /** * Build PostgreSQL string matching conditions using ILIKE (case-insensitive) * Extracted from executor.ts:807-813 and multi-cube-builder.ts:468-474 */ buildStringCondition(E, R, A) { const S = this.buildPattern(R, A); switch (R) { case "contains": return n`${E} ILIKE ${S}`; case "notContains": return n`${E} NOT ILIKE ${S}`; case "startsWith": return n`${E} ILIKE ${S}`; case "endsWith": return n`${E} ILIKE ${S}`; default: throw new Error(`Unsupported string operator: ${R}`); } } /** * Build PostgreSQL type casting using :: syntax * Extracted from various locations where ::timestamp was used */ castToType(E, R) { switch (R) { case "timestamp": return n`${E}::timestamp`; case "decimal": return n`${E}::decimal`; case "integer": return n`${E}::integer`; default: throw new Error(`Unsupported cast type: ${R}`); } } /** * Build PostgreSQL AVG aggregation with COALESCE for NULL handling * PostgreSQL AVG returns NULL for empty sets, so we use COALESCE for consistent behavior * Extracted from multi-cube-builder.ts:284 */ buildAvg(E) { return n`COALESCE(AVG(${E}), 0)`; } /** * Build PostgreSQL CASE WHEN conditional expression */ buildCaseWhen(E, R) { const A = E.map((S) => n`WHEN ${S.when} THEN ${S.then}`).reduce((S, I) => n`${S} ${I}`); return R !== void 0 ? n`CASE ${A} ELSE ${R} END` : n`CASE ${A} END`; } /** * Build PostgreSQL boolean literal * PostgreSQL uses TRUE/FALSE keywords */ buildBooleanLiteral(E) { return E ? n`TRUE` : n`FALSE`; } /** * Convert filter values - PostgreSQL uses native types * No conversion needed for PostgreSQL */ convertFilterValue(E) { return E; } /** * Prepare date value for PostgreSQL * PostgreSQL accepts Date objects directly */ prepareDateValue(E) { return E; } /** * PostgreSQL stores timestamps as native timestamp types */ isTimestampInteger() { return !1; } /** * PostgreSQL time dimensions already return proper values * No conversion needed */ convertTimeDimensionResult(E) { return E; } } class ZT extends pE { getEngineType() { return "mysql"; } /** * Build MySQL time dimension using DATE_FORMAT function * MySQL equivalent to PostgreSQL's DATE_TRUNC */ buildTimeDimension(E, R) { const A = { year: "%Y-01-01 00:00:00", quarter: "%Y-%q-01 00:00:00", // %q gives quarter (1,2,3,4), but we need to map this properly month: "%Y-%m-01 00:00:00", week: "%Y-%u-01 00:00:00", // %u gives week of year day: "%Y-%m-%d 00:00:00", hour: "%Y-%m-%d %H:00:00", minute: "%Y-%m-%d %H:%i:00", second: "%Y-%m-%d %H:%i:%s" }; switch (E) { case "quarter": return n`DATE_ADD(MAKEDATE(YEAR(${R}), 1), INTERVAL (QUARTER(${R}) - 1) * 3 MONTH)`; case "week": return n`DATE_SUB(${R}, INTERVAL WEEKDAY(${R}) DAY)`; default: const S = A[E]; return S ? n`STR_TO_DATE(DATE_FORMAT(${R}, ${S}), '%Y-%m-%d %H:%i:%s')` : R; } } /** * Build MySQL string matching conditions using LIKE * MySQL LIKE is case-insensitive by default (depending on collation) * For guaranteed case-insensitive matching, we use LOWER() functions */ buildStringCondition(E, R, A) { const S = this.buildPattern(R, A.toLowerCase()); switch (R) { case "contains": return n`LOWER(${E}) LIKE ${S}`; case "notContains": return n`LOWER(${E}) NOT LIKE ${S}`; case "startsWith": return n`LOWER(${E}) LIKE ${S}`; case "endsWith": return n`LOWER(${E}) LIKE ${S}`; default: throw new Error(`Unsupported string operator: ${R}`); } } /** * Build MySQL type casting using CAST() function * MySQL equivalent to PostgreSQL's :: casting syntax */ castToType(E, R) { switch (R) { case "timestamp": return n`CAST(${E} AS DATETIME)`; case "decimal": return n`CAST(${E} AS DECIMAL(10,2))`; case "integer": return n`CAST(${E} AS SIGNED INTEGER)`; default: throw new Error(`Unsupported cast type: ${R}`); } } /** * Build MySQL AVG aggregation with IFNULL for NULL handling * MySQL AVG returns NULL for empty sets, using IFNULL for consistency */ buildAvg(E) { return n`IFNULL(AVG(${E}), 0)`; } /** * Build MySQL CASE WHEN conditional expression */ buildCaseWhen(E, R) { const A = E.map((S) => n`WHEN ${S.when} THEN ${S.then}`).reduce((S, I) => n`${S} ${I}`); return R !== void 0 ? n`CASE ${A} ELSE ${R} END` : n`CASE ${A} END`; } /** * Build MySQL boolean literal * MySQL uses TRUE/FALSE keywords (equivalent to 1/0) */ buildBooleanLiteral(E) { return E ? n`TRUE` : n`FALSE`; } /** * Convert filter values - MySQL uses native types * No conversion needed for MySQL */ convertFilterValue(E) { return E; } /** * Prepare date value for MySQL * MySQL accepts Date objects directly */ prepareDateValue(E) { return E; } /** * MySQL stores timestamps as native timestamp types */ isTimestampInteger() { return !1; } /** * MySQL time dimensions already return proper values * No conversion needed */ convertTimeDimensionResult(E) { return E; } } class qT extends pE { getEngineType() { return "sqlite"; } /** * Build SQLite time dimension using date/datetime functions with modifiers * For integer timestamp columns (milliseconds), first convert to datetime * SQLite doesn't have DATE_TRUNC like PostgreSQL, so we use strftime and date modifiers * Returns datetime strings for consistency with other databases */ buildTimeDimension(E, R) { switch (E) { case "year": return n`datetime(${R}, 'unixepoch', 'start of year')`; case "quarter": const A = n`datetime(${R}, 'unixepoch')`; return n`datetime(${A}, 'start of year', '+' || (((CAST(strftime('%m', ${A}) AS INTEGER) - 1) / 3) * 3) || ' months')`; case "month": return n`datetime(${R}, 'unixepoch', 'start of month')`; case "week": return n`date(datetime(${R}, 'unixepoch'), 'weekday 1', '-6 days')`; case "day": return n`datetime(${R}, 'unixepoch', 'start of day')`; case "hour": const S = n`datetime(${R}, 'unixepoch')`; return n`datetime(strftime('%Y-%m-%d %H:00:00', ${S}))`; case "minute": const I = n`datetime(${R}, 'unixepoch')`; return n`datetime(strftime('%Y-%m-%d %H:%M:00', ${I}))`; case "second": const N = n`datetime(${R}, 'unixepoch')`; return n`datetime(strftime('%Y-%m-%d %H:%M:%S', ${N}))`; default: return n`datetime(${R}, 'unixepoch')`; } } /** * Build SQLite string matching conditions using LOWER() + LIKE for case-insensitive matching * SQLite LIKE is case-insensitive by default, but LOWER() ensures consistency */ buildStringCondition(E, R, A) { const S = this.buildPattern(R, A.toLowerCase()); switch (R) { case "contains": return n`LOWER(${E}) LIKE ${S}`; case "notContains": return n`LOWER(${E}) NOT LIKE ${S}`; case "startsWith": return n`LOWER(${E}) LIKE ${S}`; case "endsWith": return n`LOWER(${E}) LIKE ${S}`; default: throw new Error(`Unsupported string operator: ${R}`); } } /** * Build SQLite type casting using CAST() function * SQLite has dynamic typing but supports CAST for consistency */ castToType(E, R) { switch (R) { case "timestamp": return n`datetime(${E} / 1000, 'unixepoch')`; case "decimal": return n`CAST(${E} AS REAL)`; case "integer": return n`CAST(${E} AS INTEGER)`; default: throw new Error(`Unsupported cast type: ${R}`); } } /** * Build SQLite AVG aggregation with IFNULL for NULL handling * SQLite AVG returns NULL for empty sets, using IFNULL for consistency */ buildAvg(E) { return n`IFNULL(AVG(${E}), 0)`; } /** * Build SQLite CASE WHEN conditional expression */ buildCaseWhen(E, R) { const A = E.map((S) => S.then && typeof S.then == "object" && (S.then.queryChunks || S.then._ || S.then.sql) ? n`WHEN ${S.when} THEN ${n.raw("(")}${S.then}${n.raw(")")}` : n`WHEN ${S.when} THEN ${S.then}`).reduce((S, I) => n`${S} ${I}`); return R !== void 0 ? R && typeof R == "object" && (R.queryChunks || R._ || R.sql) ? n`CASE ${A} ELSE ${n.raw("(")}${R}${n.raw(")")} END` : n`CASE ${A} ELSE ${R} END` : n`CASE ${A} END`; } /** * Build SQLite boolean literal * SQLite uses 1/0 for true/false */ buildBooleanLiteral(E) { return E ? n`1` : n`0`; } /** * Convert filter values to SQLite-compatible types * SQLite doesn't support boolean types - convert boolean to integer (1/0) * Convert Date objects to milliseconds for integer timestamp columns */ convertFilterValue(E) { return typeof E == "boolean" ? E ? 1 : 0 : E instanceof Date ? E.getTime() : Array.isArray(E) ? E.map((R) => this.convertFilterValue(R)) : E; } /** * Prepare date value for SQLite integer timestamp storage * Convert Date objects to milliseconds (Unix timestamp * 1000) */ prepareDateValue(E) { if (!(E instanceof Date)) { if (typeof E == "number") return E; if (typeof E == "string") return new Date(E).getTime(); throw new Error(`prepareDateValue expects a Date object, got ${typeof E}`); } return E.getTime(); } /** * SQLite stores timestamps as integers (milliseconds) */ isTimestampInteger() { return !0; } /** * Convert SQLite time dimension results back to Date objects * SQLite time dimensions return datetime strings, but clients expect Date objects */ convertTimeDimensionResult(E) { return E; } } function kT(T) { switch (T) { case "postgres": return new QT(); case "mysql": return new ZT(); case "sqlite": return new qT(); default: throw new Error(`Unsupported database engine: ${T}`); } } function uO(T, E) { return { ...E, name: E.name }; } class dE { constructor(E, R, A) { f(this, "databaseAdapter"); this.db = E, this.schema = R; const S = A || this.getEngineType(); this.databaseAdapter = kT(S); } } class jT extends dE { async execute(E, R) { if (E && typeof E == "object") { if (typeof E.execute == "function") { const S = await E.execute(); return Array.isArray(S) ? S.map((I) => this.convertNumericFields(I, R)) : S; } if (this.db && typeof this.db.execute == "function") try { const S = await this.db.execute(E); return Array.isArray(S) ? S.map((I) => this.convertNumericFields(I, R)) : S; } catch (S) { if (typeof E.getSQL == "function") { const I = E.getSQL(), N = await this.db.execute(I); return Array.isArray(N) ? N.map((C) => this.convertNumericFields(C, R)) : N; } throw S; } } if (!this.db.execute) throw new Error("PostgreSQL database instance must have an execute method"); const A = await this.db.execute(E); return Array.isArray(A) ? A.map((S) => this.convertNumericFields(S, R)) : A; } /** * Convert numeric string fields to numbers (only for measure fields) */ convertNumericFields(E, R) { if (!E || typeof E != "object") return E; const A = {}; for (const [S, I] of Object.entries(E)) R && R.includes(S) ? A[S] = this.coerceToNumber(I) : A[S] = I; return A; } /** * Coerce a value to a number if it represents a numeric type */ coerceToNumber(E) { var R, A; if (E == null || typeof E == "number") return E; if (typeof E == "bigint") return Number(E); if (E && typeof E == "object") { if (typeof E.toString == "function") { const S = E.toString(); if (/^-?\d+(\.\d+)?$/.test(S)) return S.includes(".") ? parseFloat(S) : parseInt(S, 10); } if (((R = E.constructor) == null ? void 0 : R.name) === "Numeric" || ((A = E.constructor) == null ? void 0 : A.name) === "Decimal" || "digits" in E || "sign" in E) { const S = E.toString(); return parseFloat(S); } return E; } if (typeof E == "string") { if (/^-?\d+(\.\d+)?$/.test(E)) return E.includes(".") ? parseFloat(E) : parseInt(E, 10); if (!isNaN(parseFloat(E)) && isFinite(parseFloat(E))) return parseFloat(E); } return E; } getEngineType() { return "postgres"; } } class zT extends dE { async execute(E, R) { if (E && typeof E == "object" && typeof E.execute == "function") { const A = await E.execute(); return Array.isArray(A) ? A.map((S) => this.convertNumericFields(S, R)) : A; } try { if (this.db.all) { const A = this.db.all(E); return Array.isArray(A) ? A.map((S) => this.convertNumericFields(S, R)) : A; } else { if (this.db.run) return this.db.run(E); throw new Error("SQLite database instance must have an all() or run() method"); } } catch (A) { throw new Error(`SQLite execution failed: ${A instanceof Error ? A.message : "Unknown error"}`); } } /** * Convert numeric string fields to numbers (only for measure fields) */ convertNumericFields(E, R) { if (!E || typeof E != "object") return E; const A = {}; for (const [S, I] of Object.entries(E)) R && R.includes(S) ? A[S] = this.coerceToNumber(I) : A[S] = I; return A; } /** * Coerce a value to a number if it represents a numeric type */ coerceToNumber(E) { if (E == null || typeof E == "number") return E; if (typeof E == "string") { if (/^-?\d+(\.\d+)?$/.test(E)) return E.includes(".") ? parseFloat(E) : parseInt(E, 10); if (!isNaN(parseFloat(E)) && isFinite(parseFloat(E))) return parseFloat(E); } return E; } getEngineType() { return "sqlite"; } } class ER extends dE { async execute(E, R) { if (E && typeof E == "object" && typeof E.execute == "function") { const S = await E.execute(); return Array.isArray(S) ? S.map((I) => this.convertNumericFields(I, R)) : S; } if (!this.db.execute) throw new Error("MySQL database instance must have an execute method"); const A = await this.db.execute(E); return Array.isArray(A) ? A.map((S) => this.convertNumericFields(S, R)) : A; } /** * Convert numeric string fields to numbers (measure fields + numeric dimensions) */ convertNumericFields(E, R) { if (!E || typeof E != "object") return E; const A = {}; for (const [S, I] of Object.entries(E)) R && R.includes(S) ? A[S] = this.coerceToNumber(I) : A[S] = I; return A; } /** * Coerce a value to a number if it represents a numeric type */ coerceToNumber(E) { if (E == null || typeof E == "number") return E; if (typeof E == "string") { if (/^-?\d+(\.\d+)?$/.test(E)) return E.includes(".") ? parseFloat(E) : parseInt(E, 10); if (!isNaN(parseFloat(E)) && isFinite(parseFloat(E))) return parseFloat(E); } return E; } getEngineType() { return "mysql"; } } function KE(T, E) { return new jT(T, E, "postgres"); } function yE(T, E) { return new zT(T, E, "sqlite"); } function TR(T, E) { return new ER(T, E, "mysql"); } function $E(T, E, R) { if (R) switch (R) { case "postgres": return KE(T, E); case "mysql": return TR(T, E); case "sqlite": return yE(T, E); } if (T.all && T.run) return yE(T, E); if (T.execute) return KE(T, E); throw new Error("Unable to determine database engine type. Please specify engineType parameter."); } function FO(T, E) { return { name: T, ...E }; } function gE(T) { return typeof T == "function" ? T() : T; } function RR(T, E) { if (E) return E; switch (T) { case "belongsTo": return "inner"; case "hasOne": return "left"; case "hasMany": return "left"; default: return "left"; } } function K(T, E) { return typeof T == "function" ? T(E) : T; } function YO(T, E, R) { return { ...T, cubes: E, currentCube: R }; } class AR { constructor(E) { this.databaseAdapter = E; } /** * Build dynamic selections for measures, dimensions, and time dimensions * Works for both single and multi-cube queries */ buildSelections(E, R, A) { const S = {}, I = E instanceof Map ? E : /* @__PURE__ */ new Map([[E.name, E]]); if (R.dimensions) for (const N of R.dimensions) { const [C, r] = N.split("."), O = I.get(C); if (O && O.dimensions && O.dimensions[r]) { const t = O.dimensions[r], e = K(t.sql, A); S[N] = n`${e}`.as(N); } } if (R.measures) for (const N of R.measures) { const [C, r] = N.split("."), O = I.get(C); if (O && O.measures && O.measures[r]) { const t = O.measures[r], e = this.buildMeasureExpression(t, A); S[N] = n`${e}`.as(N); } } if (R.timeDimensions) for (const N of R.timeDimensions) { const [C, r] = N.dimension.split("."), O = I.get(C); if (O && O.dimensions && O.dimensions[r]) { const t = O.dimensions[r], e = this.buildTimeDimensionExpression( t.sql, N.granularity, A ); S[N.dimension] = n`${e}`.as(N.dimension); } } return Object.keys(S).length === 0 && (S.count = SE()), S; } /** * Build measure expression for HAVING clause, handling CTE references correctly */ buildHavingMeasureExpression(E, R, A, S, I) { if (I && I.preAggregationCTEs) { const N = I.preAggregationCTEs.find((C) => C.cube.name === E); if (N && N.measures.includes(`${E}.${R}`)) { const C = n`${n.identifier(N.cteAlias)}.${n.identifier(R)}`; switch (A.type) { case "count": case "countDistinct": case "sum": return y(C); case "avg": return this.databaseAdapter.buildAvg(C); case "min": return FE(C); case "max": return uE(C); case "number": return y(C); default: return y(C); } } } return this.buildMeasureExpression(A, S); } /** * Build measure expression with aggregation and filters */ buildMeasureExpression(E, R) { let A = K(E.sql, R); if (E.filters && E.filters.length > 0) { const S = E.filters.map((I) => I(R)).filter(Boolean); if (S.length > 0) { const I = S.length === 1 ? S[0] : d(...S); A = this.databaseAdapter.buildCaseWhen([ { when: I, then: A } ]); } } switch (E.type) { case "count": return SE(A); case "countDistinct": return KT(A); case "sum": return y(A); case "avg": return this.databaseAdapter.buildAvg(A); case "min": return FE(A); case "max": return uE(A); case "number": return A; default: return SE(A); } } /** * Build time dimension expression with granularity using database adapter */ buildTimeDimensionExpression(E, R, A) { const S = K(E, A); return R ? this.databaseAdapter.buildTimeDimension(R, S) : S instanceof yT ? S : n`${S}`; } /** * Build WHERE conditions from semantic query filters (dimensions only) * Works for both single and multi-cube queries */ buildWhereConditions(E, R, A, S) { const I = [], N = E instanceof Map ? E : /* @__PURE__ */ new Map([[E.name, E]]); if (R.filters && R.filters.length > 0) for (const C of R.filters) { const r = this.processFilter(C, N, A, "where", S); r && I.push(r); } if (R.timeDimensions) for (const C of R.timeDimensions) { const [r, O] = C.dimension.split("."), t = N.get(r); if (t && t.dimensions[O] && C.dateRange) { if (S != null && S.preAggregationCTEs && S.preAggregationCTEs.some((M) => M.cube.name === r)) continue; const e = t.dimensions[O], s = K(e.sql, A), i = this.buildDateRangeCondition(s, C.dateRange); i && I.push(i); } } return I; } /** * Build HAVING conditions from semantic query filters (measures only) * Works for both single and multi-cube queries */ buildHavingConditions(E, R, A, S) { const I = [], N = E instanceof Map ? E : /* @__PURE__ */ new Map([[E.name, E]]); if (R.filters && R.filters.length > 0) for (const C of R.filters) { const r = this.processFilter(C, N, A, "having", S); r && I.push(r); } return I; } /** * Process a single filter (basic or logical) * @param filterType - 'where' for dimension filters, 'having' for measure filters */ processFilter(E, R, A, S, I) { if ("and" in E || "or" in E) { const i = E; if (i.and) { const a = i.and.map((M) => this.processFilter(M, R, A, S, I)).filter((M) => M !== null); return a.length > 0 ? d(...a) : null; } if (i.or) { const a = i.or.map((M) => this.processFilter(M, R, A, S, I)).filter((M) => M !== null); return a.length > 0 ? $T(...a) : null; } } const N = E, [C, r] = N.member.split("."), O = R.get(C); if (!O) return null; const t = O.dimensions[r], e = O.measures[r], s = t || e; if (!s) return null; if (S === "where" && t) { if (I != null && I.preAggregationCTEs && I.preAggregationCTEs.some((M) => M.cube.name === C)) return null; const i = K(t.sql, A); return this.buildFilterCondition(i, N.operator, N.values, s); } else { if (S === "where" && e) return null; if (S === "having" && e) { const i = this.buildHavingMeasureExpression(C, r, e, A, I); return this.buildFilterCondition(i, N.operator, N.values, s); } } return null; } /** * Build filter condition using Drizzle operators */ buildFilterCondition(E, R, A, S) { if (!A || A.length === 0) return R === "equals" ? this.databaseAdapter.buildBooleanLiteral(!1) : null; const I = A.filter((C) => !(C == null || C === "" || typeof C == "string" && C.includes("\0"))).map(this.databaseAdapter.convertFilterValue); if (I.length === 0 && !["set", "notSet"].includes(R)) return R === "equals" ? this.databaseAdapter.buildBooleanLiteral(!1) : null; const N = I[0]; switch (R) { case "equals": if (I.length > 1) { if ((S == null ? void 0 : S.type) === "time") { const C = I.map((r) => this.normalizeDate(r) || r); return XE(E, C); } return XE(E, I); } else if (I.length === 1) { const C = (S == null ? void 0 : S.type) === "time" && this.normalizeDate(N) || N; return mE(E, C); } return this.databaseAdapter.buildBooleanLiteral(!1); case "notEquals": return I.length > 1 ? xT(E, I) : I.length === 1 ? wT(E, N) : null; case "contains": return this.databaseAdapter.buildStringCondition(E, "contains", N); case "notContains": return this.databaseAdapter.buildStringCondition(E, "notContains", N); case "startsWith": return this.databaseAdapter.buildStringCondition(E, "startsWith", N); case "endsWith": return this.databaseAdapter.buildStringCondition(E, "endsWith", N); case "gt": return fE(E, N); case "gte": return Q(E, N); case "lt": return WE(E, N); case "lte": return Z(E, N); case "set": return JT(E); case "notSet": return gT(E); case "inDateRange": if (I.length >= 2) { const C = this.normalizeDate(I[0]), r = this.normalizeDate(I[1]); if (C && r) return d( Q(E, C), Z(E, r) ); } return null; case "beforeDate": { const C = this.normalizeDate(N); return C ? WE(E, C) : null; } case "afterDate": { const C = this.normalizeDate(N); return C ? fE(E, C) : null; } default: return null; } } /** * Build date range condition for time dimensions */ buildDateRangeCondition(E, R) { if (!R) return null; if (Array.isArray(R) && R.length >= 2) { const A = this.normalizeDate(R[0]), S = this.normalizeDate(R[1]); return !A || !S ? null : d( Q(E, A), Z(E, S) ); } if (typeof R == "string") { const A = this.parseRelativeDateRange(R); if (A) return d( Q(E, A.start), Z(E, A.end) ); const S = this.normalizeDate(R); if (!S) return null; const I = new Date(S); I.setUTCHours(0, 0, 0, 0); const N = new Date(S); return N.setUTCHours(23, 59, 59, 999), d( Q(E, I), Z(E, N) ); } return null; } /** * Parse relative date range expressions like "today", "yesterday", "last 7 days", "this month", etc. * Handles all 14 DATE_RANGE_OPTIONS from the client */ parseRelativeDateRange(E) { const R = /* @__PURE__ */ new Date(), A = E.toLowerCase().trim(), S = R.getUTCFullYear(), I = R.getUTCMonth(), N = R.getUTCDate(), C = R.getUTCDay(); if (A === "today") { const e = new Date(R); e.setUTCHours(0, 0, 0, 0); const s = new Date(R); return s.setUTCHours(23, 59, 59, 999), { start: e, end: s }; } if (A === "yesterday") { const e = new Date(R); e.setUTCDate(N - 1), e.setUTCHours(0, 0, 0, 0); const s = new Date(R); return s.setUTCDate(N - 1), s.setUTCHours(23, 59, 59, 999), { start: e, end: s }; } if (A === "this week") { const e = C === 0 ? -6 : 1 - C, s = new Date(R); s.setUTCDate(N + e), s.setUTCHours(0, 0, 0, 0); const i = new Date(s); return i.setUTCDate(s.getUTCDate() + 6), i.setUTCHours(23, 59, 59, 999), { start: s, end: i }; } if (A === "this month") { const e = new Date(Date.UTC(S, I, 1, 0, 0, 0, 0)), s = new Date(Date.UTC(S, I + 1, 0, 23, 59, 59, 999)); return { start: e, end: s }; } if (A === "this quarter") { const e = Math.floor(I / 3), s = new Date(Date.UTC(S, e * 3, 1, 0, 0, 0, 0)), i = new Date(Date.UTC(S, e * 3 + 3, 0, 23, 59, 59, 999)); return { start: s, end: i }; } if (A === "this year") { const e = new Date(Date.UTC(S, 0, 1, 0, 0, 0, 0)), s = new Date(Date.UTC(S, 11, 31, 23, 59, 59, 999)); return { start: e, end: s }; } const r = A.match(/^last\s+(\d+)\s+days?$/); if (r) { const e = parseInt(r[1], 10), s = new Date(R); s.setUTCDate(N - e + 1), s.setUTCHours(0, 0, 0, 0); const i = new Date(R); return i.setUTCHours(23, 59, 59, 999), { start: s, end: i }; } if (A === "last week") { const e = C === 0 ? -13 : -6 - C, s = new Date(R); s.setUTCDate(N + e), s.setUTCHours(0, 0, 0, 0); const i = new Date(s); return i.setUTCDate(s.getUTCDate() + 6), i.setUTCHours(23, 59, 59, 999), { start: s, end: i }; } if (A === "last month") { const e = new Date(Date.UTC(S, I - 1, 1, 0, 0, 0, 0)), s = new Date(Date.UTC(S, I, 0, 23, 59, 59, 999)); return { start: e, end: s }; } if (A === "last quarter") { const e = Math.floor(I / 3), s = e === 0 ? 3 : e - 1, i = e === 0 ? S - 1 : S, a = new Date(Date.UTC(i, s * 3, 1, 0, 0, 0, 0)), M = new Date(Date.UTC(i, s * 3 + 3, 0, 23, 59, 59, 999)); return { start: a, end: M }; } if (A === "last year") { const e = new Date(Date.UTC(S - 1, 0, 1, 0, 0, 0, 0)), s = new Date(Date.UTC(S - 1, 11, 31, 23, 59, 59, 999)); return { start: e, end: s }; } if (A === "last 12 months") { const e = new Date(Date.UTC(S, I - 11, 1, 0, 0, 0, 0)), s = new Date(R); return s.setUTCHours(23, 59, 59, 999), { start: e, end: s }; } const O = A.match(/^last\s+(\d+)\s+months?$/); if (O) { const e = parseInt(O[1], 10), s = new Date(Date.UTC(S, I - e + 1, 1, 0, 0, 0, 0)), i = new Date(R); return i.setUTCHours(23, 59, 59, 999), { start: s, end: i }; } const t = A.match(/^last\s+(\d+)\s+years?$/); if (t) { const e = parseInt(t[1], 10), s = new Date(Date.UTC(S - e, 0, 1, 0, 0, 0, 0)), i = new Date(R); return i.setUTCHours(23, 59, 59, 999), { start: s, end: i }; } return null; } /** * Normalize date values to handle strings, numbers, and Date objects * Always returns a JavaScript Date object or null * Database-agnostic - just ensures we have a valid Date */ normalizeDate(E) { if (!E) return null; if (E instanceof Date) return isNaN(E.getTime()) ? null : E; if (typeof E == "number") { const A = E < 1e10 ? E * 1e3 : E, S = new Date(A); return isNaN(S.getTime()) ? null : S; } if (typeof E == "string") { const A = new Date(E); return isNaN(A.getTime()) ? null : A; } const R = new Date(E); return isNaN(R.getTime()) ? null : R; } /** * Build GROUP BY fields from dimensions and time dimensions * Works for both single and multi-cube queries */ buildGroupByFields(E, R, A, S) { var r, O; const I = []; if (!(R.measures && R.measures.length > 0)) return []; const C = E instanceof Map ? E : /* @__PURE__ */ new Map([[E.name, E]]); if (R.dimensions) for (const t of R.dimensions) { const [e, s] = t.split("."), i = C.get(e); if (i && i.dimensions && i.dimensions[s]) if ((r = S == null ? void 0 : S.preAggregationCTEs) == null ? void 0 : r.some((M) => M.cube.name === e)) { const M = S.preAggregationCTEs.find((G) => G.cube.name === e), o = M.joinKeys.find((G) => G.targetColumn === s); if (o && o.sourceColumnObj) I.push(o.sourceColumnObj); else { const G = n`${n.identifier(M.cteAlias)}.${n.identifier(s)}`; I.push(G); } } else { const M = i.dimensions[s], o = K(M.sql, A); I.push(o); } } if (R.timeDimensions) for (const t of R.timeDimensions) { const [e, s] = t.dimension.split("."), i = C.get(e); if (i && i.dimensions && i.dimensions[s]) if ((O = S == null ? void 0 : S.preAggregationCTEs) == null ? void 0 : O.some((M) => M.cube.name === e)) { const M = S.preAggregationCTEs.find((G) => G.cube.name === e), o = M.joinKeys.find((G) => G.targetColumn === s); if (o && o.sourceColumnObj) { const G = this.buildTimeDimensionExpression( o.sourceColumnObj, t.granularity, A ); I.push(G); } else { const G = n`${n.identifier(M.cteAlias)}.${n.identifier(s)}`; I.push(G); } } else { const M = i.dimensions[s], o = this.buildTimeDimensionExpression( M.sql, t.granularity, A ); I.push(o); } } return I; } /** * Build ORDER BY clause with automatic time dimension sorting */ buildOrderBy(E, R) { var I; const A = [], S = R || [ ...E.measures || [], ...E.dimensions || [], ...((I = E.timeDimensions) == null ? void 0 : I.map((N) => N.dimension)) || [] ]; if (E.order && Object.keys(E.order).length > 0) for (const [N, C] of Object.entries(E.order)) { if (!S.includes(N)) throw new Error(`Cannot order by '${N}': field is not selected in the query`); const r = C === "desc" ? vT(n.identifier(N)) : bE(n.identifier(N)); A.push(r); } if (E.timeDimensions && E.timeDimensions.length > 0) { const N = new Set(Object.keys(E.order || {})), C = [...E.timeDimensions].sort( (r, O) => r.dimension.localeCompare(O.dimension) ); for (const r of C) N.has(r.dimension) || A.push(bE(n.identifier(r.dimension))); } return A; } /** * Collect numeric field names (measures + numeric dimensions) for type conversion * Works for both single and multi-cube queries */ collectNumericFields(E, R) { const A = [], S = E instanceof Map ? E : /* @__PURE__ */ new Map([[E.name, E]]); if (R.measures && A.push(...R.measures), R.dimensions) for (const I of R.dimensions) { const [N, C] = I.split("."), r = S.get(N); if (r) { const O = r.dimensions[C]; O && O.type === "number" && A.push(I); } } return A; } /** * Apply LIMIT and OFFSET to a query with validation * If offset is provided without limit, add a reasonable default limit */ applyLimitAndOffset(E, R) { let A = R.limit; R.offset !== void 0 && R.offset > 0 && A === void 0 && (A = 50); let S = E; if (A !== void 0) { if (A < 0) throw new Error("Limit must be non-negative"); S = S.limit(A); } if (R.offset !== void 0) { if (R.offset < 0) throw new Error("Offset must be non-negative"); S = S.offset(R.offset); } return S; } } class SR { /** * Analyze a semantic query to determine which cubes are involved */ analyzeCubeUsage(E) { const R = /* @__PURE__ */ new Set(); if (E.measures) for (const A of E.measures) { const [S] = A.split("."); R.add(S); } if (E.dimensions) for (const A of E.dimensions) { const [S] = A.split("."); R.add(S); } if (E.timeDimensions) for (const A of E.timeDimensions) { const [S] = A.dimension.split("."); R.add(S); } if (E.filters) for (const A of E.filters) this.extractCubeNamesFromFilter(A, R); return R; } /** * Recursively extract cube names from filters (handles logical filters) */ extractCubeNamesFromFilter(E, R) { if ("and" in E || "or" in E) { const A = E.and || E.or || []; for (const S of A) this.extractCubeNamesFromFilter(S, R); return; } if ("member" in E) { const [A] = E.member.split("."); A && R.add(A); } } /** * Extract measures referenced in filters (for CTE inclusion) */ extractMeasuresFromFilters(E, R) { const A = []; if (!E.filters) return A; for (const S of E.filters) this.extractMeasuresFromFilter(S, R, A); return A; } /** * Recursively extract measures from filters for a specific cube */ extractMeasuresFromFilter(E, R, A) { if ("and" in E || "or" in E) { const S = E.and || E.or || []; for (const I of S) this.extractMeasuresFromFilter(I, R, A); return; } if ("member" in E) { const S = E.member, [I] = S.split("."); I === R && A.push(S); } } /** * Create a unified query plan that works for both single and multi-cube queries */ createQueryPlan(E, R, A) { const S = this.analyzeCubeUsage(R), I = Array.from(S); if (I.length === 0) throw new Error("No cubes found in query"); const N = this.choosePrimaryCube(I, R, E), C = E.get(N); if (!C) throw new Error(`Primary cube '${N}' not found`); if (I.length === 1) return { primaryCube: C, joinCubes: [], // Empty for single cube selections: {}, // Will be built by QueryBuilder whereConditions: [], // Will be built by QueryBuilder groupByFields: [] // Will be built by QueryBuilder }; const r = this.buildJoinPlan(E, C, I, A), O = this.planPreAggregationCTEs(E, C, r, R); return { primaryCube: C, joinCubes: r, selections: {}, // Will be built by QueryBuilder whereConditions: [], // Will be built by QueryBuilder groupByFields: [], // Will be built by QueryBuilder preAggregationCTEs: O }; } /** * Choose the primary cube based on query analysis * Uses a consistent strategy to avoid measure order dependencies */ choosePrimaryCube(E, R, A) { if (R.dimensions && R.dimensions.length > 0 && A) { const S = R.dimensions.map((N) => N.split(".")[0]), I = /* @__PURE__ */ new Map(); for (const N of S) I.set(N, (I.get(N) || 0) + 1); if (I.size > 0) { const N = Math.max(...I.values()), C = [...I.entries()].filter(([r, O]) => O === N).map(([r, O]) => r).sort(); for (const r of C) if (this.canReachAllCubes(r, E, A)) return r; } } if (A) { const S = /* @__PURE__ */ new Map(); for (const I of E) if (this.canReachAllCubes(I, E, A)) { const N = A.get(I), C = N != null && N.joins ? Object.keys(N.joins).length : 0; S.set(I, C); } if (S.size > 0) { const I = Math.max(...S.values()); return [...S.entries()].filter(([C, r]) => r === I).map(([C, r]) => C).sort()[0]; } } return [...E].sort()[0]; } /** * Check if a cube can reach all other cubes in the list via joins */ canReachAllCubes(E, R, A) { const S = R.filter((I) => I !== E); for (const I of S) { const N = this.findJoinPath(A, E, I, /* @__PURE__ */ new Set()); if (!N || N.length === 0) return !1; } return !0; } /** * Build join plan for multi-cube query * Supports both direct joins and transitive joins through intermediate cubes */ buildJoinPlan(E, R, A, S) { const I = [], N = /* @__PURE__ */ new Set([R.name]), C = A.filter((r) => r !== R.name); for (const r of C) { if (N.has(r)) continue; const O = this.findJoinPath(E, R.name, r, N); if (!O || O.length === 0) throw new Error(`No join path found from '${R.name}' to '${r}'`); for (const { fromCube: t, toCube: e, joinDef: s } of O) { if (N.has(e)) continue; const i = E.get(e); if (!i) throw new Error(`Cube '${e}' not found`); const a = this.buildJoinCondition( s, null, // No source alias needed - use the actual column null, // No target alias needed - use the actual column S ), M = RR(s.relationship, s.sqlJoinType); I.push({ cube: i, alias: `${e.toLowerCase()}_cube`, joinType: M, joinCondition: a }), N.add(e); } } return I; } /** * Build join condition from new array-based join definition */ buildJoinCondition(E, R, A, S) { const I = []; for (const N of E.on) { const C = R ? n`${n.identifier(R)}.${n.identifier(N.source.name)}` : N.source, r = A ? n`${n.identifier(A)}.${n.identifier(N.target.name)}` : N.target, O = N.as || mE; I.push(O(C, r)); } return d(...I); } /** * Find join path from source cube to target cube * Returns array of join steps to reach target */ findJoinPath(E, R, A, S) { if (R === A) return []; const I = [ { cube: R, path: [] } ], N = /* @__PURE__ */ new Set([R, ...S]); for (; I.length > 0; ) { const { cube: C, path: r } = I.shift(), O = E.get(C); if (O != null && O.joins) for (const [t, e] of Object.entries(O.joins)) { const i = gE(e.targetCube).name; if (N.has(i)) continue; const a = [...r, { fromCube: C, toCube: i, joinDef: e }]; if (i === A) return a; N.add(i), I.push({ cube: i, path: a }); } } return null; } /** * Plan pre-aggregation CTEs for hasMany relationships to prevent fan-out */ planPreAggregationCTEs(E, R, A, S) { const I = []; if (!S.measures || S.measures.length === 0) return I; for (const N of A) { const C = this.findHasManyJoinDef(R, N.cube.name); if (!C) continue; const r = S.measures ? S.measures.filter( (s) => s.startsWith(N.cube.name + ".") ) : [], O = this.extractMeasuresFromFilters(S, N.cube.name), t = [.../* @__PURE__ */ new Set([...r, ...O])]; if (t.length === 0) continue; const e = C.on.map((s) => ({ sourceColumn: s.source.name, targetColumn: s.target.name, sourceColumnObj: s.source, targetColumnObj: s.target })); I.push({ cube: N.cube, alias: N.alias, cteAlias: `${N.cube.name.toLowerCase()}_agg`, joinKeys: e, measures: t }); } return I; } /** * Find hasMany join definition from primary cube to target cube */ findHasManyJoinDef(E, R) { if (!E.joins) return null; for (const [A, S] of Object.entries(E.joins)) if (gE(S.targetCube).name === R && S.relationship === "hasMany") return S; return null; } } class IE { constructor(E) { f(this, "queryBuilder"); f(this, "queryPlanner"); f(this, "databaseAdapter"); if (this.dbExecutor = E, this.databaseAdapter = E.databaseAdapter, !this.databaseAdapter) throw new Error("DatabaseExecutor must have a databaseAdapter property"); this.queryBuilder = new AR(this.databaseAdapter), this.queryPlanner = new SR(); } /** * Unified query execution method that handles both single and multi-cube queries */ async execute(E, R, A) { try { const S = hT(E, R); if (!S.isValid) throw new Error(`Query validation failed: ${S.errors.join(", ")}`); const I = { db: this.dbExecutor.db, schema: this.dbExecutor.schema, securityContext: A }, N = this.queryPlanner.createQueryPlan(E, R, I), C = this.buildUnifiedQuery(N, R, I), r = this.queryBuilder.collectNumericFields(E, R), O = await this.dbExecutor.execute(C, r), t = Array.isArray(O) ? O.map((s) => { const i = { ...s }; if (R.timeDimensions) { for (const a of R.timeDimensions) if (a.dimension in i) { let M = i[a.dimension]; if (typeof M == "string" && M.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/)) { const o = M.replace(" ", "T"), G = !o.endsWith("Z") && !o.includes("+") ? o + "Z" : o; M = new Date(G); } M = this.databaseAdapter.convertTimeDimensionResult(M), i[a.dimension] = M; } } return i; }) : [O], e = this.generateAnnotations(N, R); return { data: t, annotation: e }; } catch (S) { throw new Error(`Query execution failed: ${S instanceof Error ? S.message : "Unknown error"}`); } } /** * Legacy interface for single cube queries */ async executeQuery(E, R, A) { const S = /* @__PURE__ */ new Map(); return S.set(E.name, E), this.execute(S, R, A); } /** * Build pre-aggregation CTE for hasMany relationships */ buildPreAggregationCTE(E, R, A, S) { var M; const I = E.cube, N = I.sql(A), C = {}; for (const o of E.joinKeys) if (o.targetColumnObj) { C[o.targetColumn] = o.targetColumnObj; for (const [G, P] of Object.entries(I.dimensions || {})) P.sql === o.targetColumnObj && G !== o.targetColumn && (C[G] = n`${o.targetColumnObj}`.as(G)); } for (const o of E.measures) { const [, G] = o.split("."); if (I.measures && I.measures[G]) { const P = I.measures[G], H = this.queryBuilder.buildMeasureExpression(P, A); C[G] = n`${H}`.as(G); } } const r = I.name; if (R.dimensions) for (const o of R.dimensions) { const [G, P] = o.split("."); if (G === r && I.dimensions && I.dimensions[P]) { const H = I.dimensions[P], l = this.queryBuilder.buildMeasureExpression({ sql: H.sql, type: "number" }, A); C[P] = n`${l}`.as(P); } } if (R.timeDimensions) for (const o of R.timeDimensions) { const [G, P] = o.dimension.split("."); if (G === r && I.dimensions && I.dimensions[P]) { const H = I.dimensions[P], l = this.queryBuilder.buildTimeDimensionExpression(H.sql, o.granularity, A); C[P] = n`${l}`.as(P); } } if (Object.keys(C).length === 0) return null; let O = A.db.select(C).from(N.from); const t = S ? { ...S, preAggregationCTEs: (M = S.preAggregationCTEs) == null ? void 0 : M.filter((o) => o.cube.name !== I.name) } : void 0, e = this.queryBuilder.buildWhereConditions(I, R, A, t), s = []; if (R.timeDimensions) for (const o of R.timeDimensions) { const [G, P] = o.dimension.split("."); if (G === r && I.dimensions && I.dimensions[P] && o.dateRange) { const H = I.dimensions[P], l = this.queryBuilder.buildMeasureExpression({ sql: H.sql, type: "number" }, A), B = this.queryBuilder.buildDateRangeCondition(l, o.dateRange); B && s.push(B); } } if (R.filters) { for (const o of R.filters) if (!("and" in o) && !("or" in o) && "member" in o && "operator" in o) { const G = o, [P, H] = G.member.split("."); if (P === r && I.dimensions && I.dimensions[H]) { const l = I.dimensions[H]; if (G.operator === "inDateRange") { const B = this.queryBuilder.buildMeasureExpression({ sql: l.sql, type: "number" }, A), u = this.queryBuilder.buildDateRangeCondition(B, G.values); u && s.push(u); } } } } const i = []; if (N.where && i.push(N.where), i.push(...e, ...s), i.length > 0) { const o = i.length === 1 ? i[0] : d(...i); O = O.where(o); } const a = []; for (const o of E.joinKeys) o.targetColumnObj && a.push(o.targetColumnObj); if (R.dimensions) for (const o of R.dimensions) { const [G, P] = o.split("."); if (G === r && I.dimensions && I.dimensions[P]) { const H = I.dimensions[P], l = K(H.sql, A); a.push(l); } } if (R.timeDimensions) for (const o of R.timeDimensions) { const [G, P] = o.dimension.split("."); if (G === r && I.dimensions && I.dimensions[P]) { const H = I.dimensions[P], l = this.queryBuilder.buildTimeDimensionExpression(H.sql, o.granularity, A); a.push(l); } } return a.length > 0 && (O = O.groupBy(...a)), A.db.$with(E.cteAlias).as(O); } // Removed unused getActualJoinTargetColumn method /** * Build join condition for CTE */ buildCTEJoinCondition(E, R, A) { var N; const S = (N = A.preAggregationCTEs) == null ? void 0 : N.find((C) => C.cube.name === E.cube.name); if (!S) throw new Error(`CTE info not found for cube ${E.cube.name}`); const I = []; for (const C of S.joinKeys) { const r = C.sourceColumnObj || n.identifier(C.sourceColumn), O = n`${n.identifier(R)}.${n.identifier(C.targetColumn)}`; I.push(mE(r, O)); } return I.length === 1 ? I[0] : d(...I); } /** * Build unified query that works for both single and multi-cube queries */ buildUnifiedQuery(E, R, A) { var M, o, G; const S = [], I = /* @__PURE__ */ new Map(); if (E.preAggregationCTEs && E.preAggregationCTEs.length > 0) for (const P of E.preAggregationCTEs) { const H = this.buildPreAggregationCTE(P, R, A, E); H && (S.push(H), I.set(P.cube.name, P.cteAlias)); } const N = E.primaryCube.sql(A), r = { ...this.queryBuilder.buildSelections( E.joinCubes.length > 0 ? this.getCubesFromPlan(E) : E.primaryCub