UNPKG

drizzle-cube

Version:

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

1,491 lines 518 kB
import { sql as r, eq as K, and as H, SQL as OT, gte as Q, lte as Z, arrayContained as IT, arrayOverlaps as nT, arrayContains as rT, isNotNull as zE, ne as Ee, or as eE, isNull as ee, notInArray as Te, inArray as CE, lt as aE, gt as LE, StringChunk as iT, sum as g, max as TE, min as RE, count as XE, countDistinct as CT, desc as aT, asc as Re } from "drizzle-orm"; class xE { /** * Default implementation returns template unchanged * Override in specific adapters for database-specific preprocessing */ preprocessCalculatedTemplate(E) { return E; } /** * Helper method to build pattern for string matching * Can be overridden by specific adapters if needed */ buildPattern(E, e) { switch (E) { case "contains": case "notContains": return `%${e}%`; case "startsWith": return `${e}%`; case "endsWith": return `%${e}`; default: return e; } } } class LT extends xE { 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, e) { switch (E) { case "year": return r`DATE_TRUNC('year', ${e}::timestamp)`; case "quarter": return r`DATE_TRUNC('quarter', ${e}::timestamp)`; case "month": return r`DATE_TRUNC('month', ${e}::timestamp)`; case "week": return r`DATE_TRUNC('week', ${e}::timestamp)`; case "day": return r`DATE_TRUNC('day', ${e}::timestamp)::timestamp`; case "hour": return r`DATE_TRUNC('hour', ${e}::timestamp)`; case "minute": return r`DATE_TRUNC('minute', ${e}::timestamp)`; case "second": return r`DATE_TRUNC('second', ${e}::timestamp)`; default: return e; } } /** * Build PostgreSQL string matching conditions using ILIKE (case-insensitive) * Extracted from executor.ts:807-813 and multi-cube-builder.ts:468-474 */ buildStringCondition(E, e, R) { switch (e) { case "contains": return r`${E} ILIKE ${`%${R}%`}`; case "notContains": return r`${E} NOT ILIKE ${`%${R}%`}`; case "startsWith": return r`${E} ILIKE ${`${R}%`}`; case "endsWith": return r`${E} ILIKE ${`%${R}`}`; case "like": return r`${E} LIKE ${R}`; case "notLike": return r`${E} NOT LIKE ${R}`; case "ilike": return r`${E} ILIKE ${R}`; case "regex": return r`${E} ~* ${R}`; case "notRegex": return r`${E} !~* ${R}`; default: throw new Error(`Unsupported string operator: ${e}`); } } /** * Build PostgreSQL type casting using :: syntax * Extracted from various locations where ::timestamp was used */ castToType(E, e) { switch (e) { case "timestamp": return r`${E}::timestamp`; case "decimal": return r`${E}::decimal`; case "integer": return r`${E}::integer`; default: throw new Error(`Unsupported cast type: ${e}`); } } /** * 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 r`COALESCE(AVG(${E}), 0)`; } /** * Build PostgreSQL CASE WHEN conditional expression */ buildCaseWhen(E, e) { const R = E.map((A) => r`WHEN ${A.when} THEN ${A.then}`).reduce((A, t) => r`${A} ${t}`); return e !== void 0 ? r`CASE ${R} ELSE ${e} END` : r`CASE ${R} END`; } /** * Build PostgreSQL boolean literal * PostgreSQL uses TRUE/FALSE keywords */ buildBooleanLiteral(E) { return E ? r`TRUE` : r`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; } // ============================================ // Statistical & Window Function Methods // ============================================ /** * PostgreSQL has full support for statistical and window functions */ getCapabilities() { return { supportsStddev: !0, supportsVariance: !0, supportsPercentile: !0, supportsWindowFunctions: !0, supportsFrameClause: !0 }; } /** * Build PostgreSQL STDDEV aggregation * Uses STDDEV_POP for population, STDDEV_SAMP for sample */ buildStddev(E, e = !1) { const R = e ? "STDDEV_SAMP" : "STDDEV_POP"; return r`COALESCE(${r.raw(R)}(${E}), 0)`; } /** * Build PostgreSQL VARIANCE aggregation * Uses VAR_POP for population, VAR_SAMP for sample */ buildVariance(E, e = !1) { const R = e ? "VAR_SAMP" : "VAR_POP"; return r`COALESCE(${r.raw(R)}(${E}), 0)`; } /** * Build PostgreSQL PERCENTILE_CONT aggregation * Uses ordered-set aggregate function */ buildPercentile(E, e) { const R = e / 100; return r`PERCENTILE_CONT(${R}) WITHIN GROUP (ORDER BY ${E})`; } /** * Build PostgreSQL window function expression * PostgreSQL has full window function support */ buildWindowFunction(E, e, R, A, t) { const s = R && R.length > 0 ? r`PARTITION BY ${r.join(R, r`, `)}` : r``, S = A && A.length > 0 ? r`ORDER BY ${r.join(A.map( (i) => i.direction === "desc" ? r`${i.field} DESC` : r`${i.field} ASC` ), r`, `)}` : r``; let n = r``; if (t?.frame) { const { type: i, start: C, end: a } = t.frame, _ = i.toUpperCase(), P = C === "unbounded" ? "UNBOUNDED PRECEDING" : typeof C == "number" ? `${C} PRECEDING` : "CURRENT ROW", M = a === "unbounded" ? "UNBOUNDED FOLLOWING" : a === "current" ? "CURRENT ROW" : typeof a == "number" ? `${a} FOLLOWING` : "CURRENT ROW"; n = r`${r.raw(_)} BETWEEN ${r.raw(P)} AND ${r.raw(M)}`; } const N = []; R && R.length > 0 && N.push(s), A && A.length > 0 && N.push(S), t?.frame && N.push(n); const I = N.length > 0 ? r.join(N, r` `) : r``, O = r`OVER (${I})`; switch (E) { case "lag": return r`LAG(${e}, ${t?.offset ?? 1}${t?.defaultValue !== void 0 ? r`, ${t.defaultValue}` : r``}) ${O}`; case "lead": return r`LEAD(${e}, ${t?.offset ?? 1}${t?.defaultValue !== void 0 ? r`, ${t.defaultValue}` : r``}) ${O}`; case "rank": return r`RANK() ${O}`; case "denseRank": return r`DENSE_RANK() ${O}`; case "rowNumber": return r`ROW_NUMBER() ${O}`; case "ntile": return r`NTILE(${t?.nTile ?? 4}) ${O}`; case "firstValue": return r`FIRST_VALUE(${e}) ${O}`; case "lastValue": return r`LAST_VALUE(${e}) ${O}`; case "movingAvg": return r`AVG(${e}) ${O}`; case "movingSum": return r`SUM(${e}) ${O}`; default: throw new Error(`Unsupported window function: ${E}`); } } } class Ke extends xE { getEngineType() { return "mysql"; } /** * Build MySQL time dimension using DATE_FORMAT function * MySQL equivalent to PostgreSQL's DATE_TRUNC */ buildTimeDimension(E, e) { const R = { 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 r`DATE_ADD(MAKEDATE(YEAR(${e}), 1), INTERVAL (QUARTER(${e}) - 1) * 3 MONTH)`; case "week": return r`DATE_SUB(${e}, INTERVAL WEEKDAY(${e}) DAY)`; default: { const A = R[E]; return A ? r`STR_TO_DATE(DATE_FORMAT(${e}, ${A}), '%Y-%m-%d %H:%i:%s')` : e; } } } /** * 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, e, R) { switch (e) { case "contains": return r`LOWER(${E}) LIKE ${`%${R.toLowerCase()}%`}`; case "notContains": return r`LOWER(${E}) NOT LIKE ${`%${R.toLowerCase()}%`}`; case "startsWith": return r`LOWER(${E}) LIKE ${`${R.toLowerCase()}%`}`; case "endsWith": return r`LOWER(${E}) LIKE ${`%${R.toLowerCase()}`}`; case "like": return r`${E} LIKE ${R}`; case "notLike": return r`${E} NOT LIKE ${R}`; case "ilike": return r`LOWER(${E}) LIKE ${R.toLowerCase()}`; case "regex": return r`${E} REGEXP ${R}`; case "notRegex": return r`${E} NOT REGEXP ${R}`; default: throw new Error(`Unsupported string operator: ${e}`); } } /** * Build MySQL type casting using CAST() function * MySQL equivalent to PostgreSQL's :: casting syntax */ castToType(E, e) { switch (e) { case "timestamp": return r`CAST(${E} AS DATETIME)`; case "decimal": return r`CAST(${E} AS DECIMAL(10,2))`; case "integer": return r`CAST(${E} AS SIGNED INTEGER)`; default: throw new Error(`Unsupported cast type: ${e}`); } } /** * Build MySQL AVG aggregation with IFNULL for NULL handling * MySQL AVG returns NULL for empty sets, using IFNULL for consistency */ buildAvg(E) { return r`IFNULL(AVG(${E}), 0)`; } /** * Build MySQL CASE WHEN conditional expression */ buildCaseWhen(E, e) { const R = E.map((A) => r`WHEN ${A.when} THEN ${A.then}`).reduce((A, t) => r`${A} ${t}`); return e !== void 0 ? r`CASE ${R} ELSE ${e} END` : r`CASE ${R} END`; } /** * Build MySQL boolean literal * MySQL uses TRUE/FALSE keywords (equivalent to 1/0) */ buildBooleanLiteral(E) { return E ? r`TRUE` : r`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; } // ============================================ // Statistical & Window Function Methods // ============================================ /** * MySQL 8.0+ has support for statistical and window functions * but not PERCENTILE_CONT */ getCapabilities() { return { supportsStddev: !0, supportsVariance: !0, supportsPercentile: !1, // MySQL doesn't have PERCENTILE_CONT supportsWindowFunctions: !0, supportsFrameClause: !0 }; } /** * Build MySQL STDDEV aggregation * Uses STDDEV_POP for population, STDDEV_SAMP for sample */ buildStddev(E, e = !1) { const R = e ? "STDDEV_SAMP" : "STDDEV_POP"; return r`IFNULL(${r.raw(R)}(${E}), 0)`; } /** * Build MySQL VARIANCE aggregation * Uses VAR_POP for population, VAR_SAMP for sample */ buildVariance(E, e = !1) { const R = e ? "VAR_SAMP" : "VAR_POP"; return r`IFNULL(${r.raw(R)}(${E}), 0)`; } /** * MySQL does not support PERCENTILE_CONT * Returns null for graceful degradation */ buildPercentile(E, e) { return null; } /** * Build MySQL window function expression * MySQL 8.0+ has full window function support */ buildWindowFunction(E, e, R, A, t) { const s = R && R.length > 0 ? r`PARTITION BY ${r.join(R, r`, `)}` : r``, S = A && A.length > 0 ? r`ORDER BY ${r.join(A.map( (i) => i.direction === "desc" ? r`${i.field} DESC` : r`${i.field} ASC` ), r`, `)}` : r``; let n = r``; if (t?.frame) { const { type: i, start: C, end: a } = t.frame, _ = i.toUpperCase(), P = C === "unbounded" ? "UNBOUNDED PRECEDING" : typeof C == "number" ? `${C} PRECEDING` : "CURRENT ROW", M = a === "unbounded" ? "UNBOUNDED FOLLOWING" : a === "current" ? "CURRENT ROW" : typeof a == "number" ? `${a} FOLLOWING` : "CURRENT ROW"; n = r`${r.raw(_)} BETWEEN ${r.raw(P)} AND ${r.raw(M)}`; } const N = []; R && R.length > 0 && N.push(s), A && A.length > 0 && N.push(S), t?.frame && N.push(n); const I = N.length > 0 ? r.join(N, r` `) : r``, O = r`OVER (${I})`; switch (E) { case "lag": return r`LAG(${e}, ${t?.offset ?? 1}${t?.defaultValue !== void 0 ? r`, ${t.defaultValue}` : r``}) ${O}`; case "lead": return r`LEAD(${e}, ${t?.offset ?? 1}${t?.defaultValue !== void 0 ? r`, ${t.defaultValue}` : r``}) ${O}`; case "rank": return r`RANK() ${O}`; case "denseRank": return r`DENSE_RANK() ${O}`; case "rowNumber": return r`ROW_NUMBER() ${O}`; case "ntile": return r`NTILE(${t?.nTile ?? 4}) ${O}`; case "firstValue": return r`FIRST_VALUE(${e}) ${O}`; case "lastValue": return r`LAST_VALUE(${e}) ${O}`; case "movingAvg": return r`AVG(${e}) ${O}`; case "movingSum": return r`SUM(${e}) ${O}`; default: throw new Error(`Unsupported window function: ${E}`); } } } class oT extends xE { 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, e) { switch (E) { case "year": return r`datetime(${e}, 'unixepoch', 'start of year')`; case "quarter": { const R = r`datetime(${e}, 'unixepoch')`; return r`datetime(${R}, 'start of year', '+' || (((CAST(strftime('%m', ${R}) AS INTEGER) - 1) / 3) * 3) || ' months')`; } case "month": return r`datetime(${e}, 'unixepoch', 'start of month')`; case "week": return r`date(datetime(${e}, 'unixepoch'), 'weekday 1', '-6 days')`; case "day": return r`datetime(${e}, 'unixepoch', 'start of day')`; case "hour": { const R = r`datetime(${e}, 'unixepoch')`; return r`datetime(strftime('%Y-%m-%d %H:00:00', ${R}))`; } case "minute": { const R = r`datetime(${e}, 'unixepoch')`; return r`datetime(strftime('%Y-%m-%d %H:%M:00', ${R}))`; } case "second": { const R = r`datetime(${e}, 'unixepoch')`; return r`datetime(strftime('%Y-%m-%d %H:%M:%S', ${R}))`; } default: return r`datetime(${e}, '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, e, R) { switch (e) { case "contains": return r`LOWER(${E}) LIKE ${`%${R.toLowerCase()}%`}`; case "notContains": return r`LOWER(${E}) NOT LIKE ${`%${R.toLowerCase()}%`}`; case "startsWith": return r`LOWER(${E}) LIKE ${`${R.toLowerCase()}%`}`; case "endsWith": return r`LOWER(${E}) LIKE ${`%${R.toLowerCase()}`}`; case "like": return r`${E} LIKE ${R}`; case "notLike": return r`${E} NOT LIKE ${R}`; case "ilike": return r`LOWER(${E}) LIKE ${R.toLowerCase()}`; case "regex": return r`${E} GLOB ${R}`; case "notRegex": return r`${E} NOT GLOB ${R}`; default: throw new Error(`Unsupported string operator: ${e}`); } } /** * Build SQLite type casting using CAST() function * SQLite has dynamic typing but supports CAST for consistency */ castToType(E, e) { switch (e) { case "timestamp": return r`datetime(${E} / 1000, 'unixepoch')`; case "decimal": return r`CAST(${E} AS REAL)`; case "integer": return r`CAST(${E} AS INTEGER)`; default: throw new Error(`Unsupported cast type: ${e}`); } } /** * Build SQLite AVG aggregation with IFNULL for NULL handling * SQLite AVG returns NULL for empty sets, using IFNULL for consistency */ buildAvg(E) { return r`IFNULL(AVG(${E}), 0)`; } /** * Build SQLite CASE WHEN conditional expression */ buildCaseWhen(E, e) { const R = E.map((A) => A.then && typeof A.then == "object" && (A.then.queryChunks || A.then._ || A.then.sql) ? r`WHEN ${A.when} THEN ${r.raw("(")}${A.then}${r.raw(")")}` : r`WHEN ${A.when} THEN ${A.then}`).reduce((A, t) => r`${A} ${t}`); return e !== void 0 ? e && typeof e == "object" && (e.queryChunks || e._ || e.sql) ? r`CASE ${R} ELSE ${r.raw("(")}${e}${r.raw(")")} END` : r`CASE ${R} ELSE ${e} END` : r`CASE ${R} END`; } /** * Build SQLite boolean literal * SQLite uses 1/0 for true/false */ buildBooleanLiteral(E) { return E ? r`1` : r`0`; } /** * Preprocess calculated measure templates for SQLite-specific handling * * SQLite performs integer division by default (5/2 = 2 instead of 2.5). * This method wraps division operands with CAST to REAL to ensure float division. * * Pattern matched: {measure1} / {measure2} or {measure1} / NULLIF({measure2}, 0) * Transforms to: CAST({measure1} AS REAL) / ... * * @param calculatedSql - Template string with {member} references * @returns Preprocessed template with CAST for division operations */ preprocessCalculatedTemplate(E) { const e = /(\{[^}]+\})\s*\/\s*/g; return E.replace(e, (R, A) => `${A.replace(/\{([^}]+)\}/, "CAST({$1} AS REAL)")} / `); } /** * 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((e) => this.convertFilterValue(e)) : 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; } // ============================================ // Statistical & Window Function Methods // ============================================ /** * SQLite has limited statistical support (no native STDDEV/VARIANCE/PERCENTILE) * but supports window functions since SQLite 3.25 */ getCapabilities() { return { supportsStddev: !1, // Requires extension supportsVariance: !1, // Requires extension supportsPercentile: !1, // Requires extension supportsWindowFunctions: !0, // SQLite 3.25+ supportsFrameClause: !0 }; } /** * SQLite does not have native STDDEV * Returns null for graceful degradation */ buildStddev(E, e = !1) { return null; } /** * SQLite does not have native VARIANCE * Returns null for graceful degradation */ buildVariance(E, e = !1) { return null; } /** * SQLite does not have native PERCENTILE * Returns null for graceful degradation */ buildPercentile(E, e) { return null; } /** * Build SQLite window function expression * SQLite 3.25+ supports window functions */ buildWindowFunction(E, e, R, A, t) { const s = R && R.length > 0 ? r`PARTITION BY ${r.join(R, r`, `)}` : r``, S = A && A.length > 0 ? r`ORDER BY ${r.join(A.map( (i) => i.direction === "desc" ? r`${i.field} DESC` : r`${i.field} ASC` ), r`, `)}` : r``; let n = r``; if (t?.frame) { const { type: i, start: C, end: a } = t.frame, _ = i.toUpperCase(), P = C === "unbounded" ? "UNBOUNDED PRECEDING" : typeof C == "number" ? `${C} PRECEDING` : "CURRENT ROW", M = a === "unbounded" ? "UNBOUNDED FOLLOWING" : a === "current" ? "CURRENT ROW" : typeof a == "number" ? `${a} FOLLOWING` : "CURRENT ROW"; n = r`${r.raw(_)} BETWEEN ${r.raw(P)} AND ${r.raw(M)}`; } const N = []; R && R.length > 0 && N.push(s), A && A.length > 0 && N.push(S), t?.frame && N.push(n); const I = N.length > 0 ? r.join(N, r` `) : r``, O = r`OVER (${I})`; switch (E) { case "lag": return r`LAG(${e}, ${t?.offset ?? 1}${t?.defaultValue !== void 0 ? r`, ${t.defaultValue}` : r``}) ${O}`; case "lead": return r`LEAD(${e}, ${t?.offset ?? 1}${t?.defaultValue !== void 0 ? r`, ${t.defaultValue}` : r``}) ${O}`; case "rank": return r`RANK() ${O}`; case "denseRank": return r`DENSE_RANK() ${O}`; case "rowNumber": return r`ROW_NUMBER() ${O}`; case "ntile": return r`NTILE(${t?.nTile ?? 4}) ${O}`; case "firstValue": return r`FIRST_VALUE(${e}) ${O}`; case "lastValue": return r`LAST_VALUE(${e}) ${O}`; case "movingAvg": return r`AVG(${e}) ${O}`; case "movingSum": return r`SUM(${e}) ${O}`; default: throw new Error(`Unsupported window function: ${E}`); } } } class _T extends Ke { getEngineType() { return "singlestore"; } // SingleStore inherits most MySQL functionality // Override methods here only if SingleStore-specific behavior is needed // Note: SingleStore has some known limitations: // - ORDER BY and LIMIT cannot be chained together in some contexts // - Nested selects with aggregation functions are not supported // - Serial column type only assures uniqueness (tests may need ORDER BY) // These limitations are typically handled at the query building level // rather than in the adapter, but can be addressed here if needed } function DT(T) { switch (T) { case "postgres": return new LT(); case "mysql": return new Ke(); case "sqlite": return new oT(); case "singlestore": return new _T(); default: throw new Error(`Unsupported database engine: ${T}`); } } class QE { constructor(E, e, R) { this.db = E, this.schema = e; const A = R || this.getEngineType(); this.databaseAdapter = DT(A); } databaseAdapter; } class PT extends QE { async execute(E, e) { if (E && typeof E == "object" && typeof E.execute == "function") { const A = await E.execute(); return Array.isArray(A) ? A.map((t) => this.convertNumericFields(t, e)) : A; } if (!this.db.execute) throw new Error("PostgreSQL database instance must have an execute method"); const R = await this.db.execute(E); return Array.isArray(R) ? R.map((A) => this.convertNumericFields(A, e)) : R; } /** * Convert numeric string fields to numbers (only for measure fields) */ convertNumericFields(E, e) { if (!E || typeof E != "object") return E; const R = {}; for (const [A, t] of Object.entries(E)) e && e.includes(A) ? R[A] = this.coerceToNumber(t) : R[A] = t; return R; } /** * 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 == "bigint") return Number(E); if (E && typeof E == "object") { if (typeof E.toString == "function") { const e = E.toString(); if (/^-?\d+(\.\d+)?$/.test(e)) return e.includes(".") ? parseFloat(e) : parseInt(e, 10); } if (E.constructor?.name === "Numeric" || E.constructor?.name === "Decimal" || "digits" in E || "sign" in E) { const e = E.toString(); return parseFloat(e); } 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"; } } function Ae(T, E) { return new PT(T, E, "postgres"); } class we extends QE { async execute(E, e) { if (E && typeof E == "object" && typeof E.execute == "function") { const A = await E.execute(); return Array.isArray(A) ? A.map((t) => this.convertNumericFields(t, e)) : A; } if (!this.db.execute) throw new Error("MySQL database instance must have an execute method"); const R = await this.db.execute(E); return Array.isArray(R) ? R.map((A) => this.convertNumericFields(A, e)) : R; } /** * Convert numeric string fields to numbers (measure fields + numeric dimensions) */ convertNumericFields(E, e) { if (!E || typeof E != "object") return E; const R = {}; for (const [A, t] of Object.entries(E)) e && e.includes(A) ? R[A] = this.coerceToNumber(t) : R[A] = t; return R; } /** * 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 cT(T, E) { return new we(T, E, "mysql"); } class MT extends QE { async execute(E, e) { if (E && typeof E == "object" && typeof E.execute == "function") { const R = await E.execute(); return Array.isArray(R) ? R.map((A) => this.convertNumericFields(A, e)) : R; } try { if (this.db.all) { const R = this.db.all(E); return Array.isArray(R) ? R.map((A) => this.convertNumericFields(A, e)) : R; } else { if (this.db.run) return this.db.run(E); throw new Error("SQLite database instance must have an all() or run() method"); } } catch (R) { throw new Error(`SQLite execution failed: ${R instanceof Error ? R.message : "Unknown error"}`); } } /** * Convert numeric string fields to numbers (only for measure fields) */ convertNumericFields(E, e) { if (!E || typeof E != "object") return E; const R = {}; for (const [A, t] of Object.entries(E)) e && e.includes(A) ? R[A] = this.coerceToNumber(t) : R[A] = t; return R; } /** * 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"; } } function te(T, E) { return new MT(T, E, "sqlite"); } class UT extends we { getEngineType() { return "singlestore"; } // SingleStore-specific optimizations can be added here if needed // For now, we inherit all behavior from MySQLExecutor since // SingleStore is largely MySQL-compatible } function lT(T, E) { return new UT(T, E); } function se(T, E, e) { if (e) switch (e) { case "postgres": return Ae(T, E); case "mysql": return cT(T, E); case "sqlite": return te(T, E); case "singlestore": return lT(T, E); } if (T.all && T.run) return te(T, E); if (T.execute) return Ae(T, E); throw new Error("Unable to determine database engine type. Please specify engineType parameter."); } function NE(T) { return typeof T == "function" ? T() : T; } function $E(T, E) { if (E) return E; switch (T) { case "belongsTo": return "inner"; // FK should exist case "hasOne": return "left"; // Parent may have no child case "hasMany": return "left"; // Parent may have no children case "belongsToMany": return "left"; // Many-to-many through junction table default: return "left"; } } function KE(T) { return T && typeof T == "object" ? r`${r`${T}`}` : T; } function Y(T, E) { const e = typeof T == "function" ? T(E) : T; return KE(e); } function hS(T, E, e) { return { ...T, cubes: E, currentCube: e }; } function HS(T, E) { return { name: T, ...E }; } function uT(T, E) { if (T.relationship !== "belongsToMany" || !T.through) throw new Error("expandBelongsToManyJoin can only be called on belongsToMany relationships with through configuration"); const { table: e, sourceKey: R, targetKey: A, securitySql: t } = T.through, s = []; for (const I of R) { const O = I.as || K; s.push(O(I.source, I.target)); } const S = []; for (const I of A) { const O = I.as || K; S.push(O(I.source, I.target)); } let n; if (t) { const I = t(E); n = Array.isArray(I) ? I : [I]; } const N = $E("belongsToMany", T.sqlJoinType); return { junctionJoins: [ { joinType: N, table: e, condition: H(...s) }, { joinType: N, table: e, // This will be replaced with target cube table in query planner condition: H(...S) } ], junctionSecurityConditions: n }; } function OE(T) { if ("and" in T) return `and:[${T.and.map(OE).sort().join(",")}]`; if ("or" in T) return `or:[${T.or.map(OE).sort().join(",")}]`; const E = T, e = JSON.stringify( Array.isArray(E.values) ? [...E.values].sort() : E.values ), R = E.dateRange ? `:dr:${JSON.stringify(E.dateRange)}` : ""; return `${E.member}:${E.operator}:${e}${R}`; } function Je(T, E) { return `timeDim:${T}:${JSON.stringify(E)}`; } class Se { cache = /* @__PURE__ */ new Map(); stats = { hits: 0, misses: 0 }; /** * Get cached SQL or build and cache it * * @param key - The cache key (use getFilterKey or getTimeDimensionFilterKey) * @param builder - Function to build the SQL if not cached * @returns The cached or freshly built SQL, or null if builder returns null */ getOrBuild(E, e) { const R = this.cache.get(E); if (R !== void 0) return this.stats.hits++, R; const A = e(); return A && this.cache.set(E, A), this.stats.misses++, A; } /** * Check if a key exists in the cache without affecting stats */ has(E) { return this.cache.has(E); } /** * Get cached SQL without building (returns undefined if not cached) */ get(E) { const e = this.cache.get(E); return e !== void 0 && this.stats.hits++, e; } /** * Pre-populate cache with multiple filters * Useful for batch initialization at query start */ preload(E) { for (const { key: e, sql: R } of E) this.cache.has(e) || this.cache.set(e, R); } /** * Store a single entry in the cache */ set(E, e) { this.cache.set(E, e); } /** * Get cache statistics for debugging */ getStats() { return { ...this.stats, cacheSize: this.cache.size }; } /** * Clear the cache (normally not needed as cache is per-query) */ clear() { this.cache.clear(), this.stats = { hits: 0, misses: 0 }; } } function wE(T) { const E = []; for (const e of T) "and" in e && e.and ? E.push(...wE(e.and)) : "or" in e && e.or ? E.push(...wE(e.or)) : "member" in e && E.push(e); return E; } function dT(T, E, e = {}) { const R = e.keyPrefix ?? "drizzle-cube:", A = GT(T), t = Ne(JSON.stringify(A)); let s = `${R}query:${t}`; if (e.includeSecurityContext !== !1) { const S = e.securityContextSerializer ? e.securityContextSerializer(E) : JSON.stringify(IE(E)), n = Ne(S); s += `:ctx:${n}`; } return s; } function GT(T) { return { measures: T.measures ? [...T.measures].sort() : void 0, dimensions: T.dimensions ? [...T.dimensions].sort() : void 0, filters: T.filters ? JE(T.filters) : void 0, timeDimensions: T.timeDimensions ? mT(T.timeDimensions) : void 0, limit: T.limit, offset: T.offset, order: T.order ? IE(T.order) : void 0, fillMissingDatesValue: T.fillMissingDatesValue }; } function JE(T) { return [...T].map((E) => { if ("and" in E && E.and) return { and: JE(E.and) }; if ("or" in E && E.or) return { or: JE(E.or) }; const e = E; return { ...e, values: e.values ? [...e.values].sort() : e.values }; }).sort((E, e) => JSON.stringify(E).localeCompare(JSON.stringify(e))); } function mT(T) { return [...T].map((E) => ({ dimension: E.dimension, granularity: E.granularity, dateRange: E.dateRange, fillMissingDates: E.fillMissingDates, compareDateRange: E.compareDateRange ? [...E.compareDateRange].sort((e, R) => { const A = Array.isArray(e) ? e.join("-") : e, t = Array.isArray(R) ? R.join("-") : R; return A.localeCompare(t); }) : void 0 })).sort((E, e) => E.dimension.localeCompare(e.dimension)); } function IE(T) { return T === null || typeof T != "object" ? T : Array.isArray(T) ? T.map(IE) : Object.keys(T).sort().reduce((E, e) => (E[e] = IE( T[e] ), E), {}); } function Ne(T) { let E = 2166136261; for (let e = 0; e < T.length; e++) E ^= T.charCodeAt(e), E = E * 16777619 >>> 0; return E.toString(16).padStart(8, "0"); } function fS(T, E) { return `${E ?? "drizzle-cube:"}*${T}*`; } class ve { constructor(E) { this.databaseAdapter = E; } /** * Build time dimension expression with granularity using database adapter */ buildTimeDimensionExpression(E, e, R) { const A = Y(E, R); return e ? this.databaseAdapter.buildTimeDimension(e, A) : A instanceof OT ? A : r`${A}`; } /** * Build date range condition for time dimensions */ buildDateRangeCondition(E, e) { if (!e) return null; if (Array.isArray(e) && e.length >= 2) { const R = this.normalizeDate(e[0]); let A = this.normalizeDate(e[1]); if (!R || !A) return null; if (typeof e[1] == "string" && /^\d{4}-\d{2}-\d{2}$/.test(e[1].trim())) { const t = typeof A == "number" ? new Date(A * (this.databaseAdapter.getEngineType() === "sqlite" ? 1e3 : 1)) : new Date(A), s = new Date(t); s.setUTCHours(23, 59, 59, 999), this.databaseAdapter.isTimestampInteger() ? A = this.databaseAdapter.getEngineType() === "sqlite" ? Math.floor(s.getTime() / 1e3) : s.getTime() : A = s.toISOString(); } return H( Q(E, R), Z(E, A) ); } if (typeof e == "string") { const R = this.parseRelativeDateRange(e); if (R) { let I, O; return this.databaseAdapter.isTimestampInteger() ? this.databaseAdapter.getEngineType() === "sqlite" ? (I = Math.floor(R.start.getTime() / 1e3), O = Math.floor(R.end.getTime() / 1e3)) : (I = R.start.getTime(), O = R.end.getTime()) : (I = R.start.toISOString(), O = R.end.toISOString()), H( Q(E, I), Z(E, O) ); } const A = this.normalizeDate(e); if (!A) return null; const t = typeof A == "number" ? new Date(A * (this.databaseAdapter.getEngineType() === "sqlite" ? 1e3 : 1)) : new Date(A), s = new Date(t); s.setUTCHours(0, 0, 0, 0); const S = new Date(t); S.setUTCHours(23, 59, 59, 999); let n, N; return this.databaseAdapter.isTimestampInteger() ? this.databaseAdapter.getEngineType() === "sqlite" ? (n = Math.floor(s.getTime() / 1e3), N = Math.floor(S.getTime() / 1e3)) : (n = s.getTime(), N = S.getTime()) : (n = s.toISOString(), N = S.toISOString()), H( Q(E, n), 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 e = /* @__PURE__ */ new Date(), R = E.toLowerCase().trim(), A = e.getUTCFullYear(), t = e.getUTCMonth(), s = e.getUTCDate(), S = e.getUTCDay(); if (R === "today") { const i = new Date(e); i.setUTCHours(0, 0, 0, 0); const C = new Date(e); return C.setUTCHours(23, 59, 59, 999), { start: i, end: C }; } if (R === "yesterday") { const i = new Date(e); i.setUTCDate(s - 1), i.setUTCHours(0, 0, 0, 0); const C = new Date(e); return C.setUTCDate(s - 1), C.setUTCHours(23, 59, 59, 999), { start: i, end: C }; } if (R === "this week") { const i = S === 0 ? -6 : 1 - S, C = new Date(e); C.setUTCDate(s + i), C.setUTCHours(0, 0, 0, 0); const a = new Date(C); return a.setUTCDate(C.getUTCDate() + 6), a.setUTCHours(23, 59, 59, 999), { start: C, end: a }; } if (R === "this month") { const i = new Date(Date.UTC(A, t, 1, 0, 0, 0, 0)), C = new Date(Date.UTC(A, t + 1, 0, 23, 59, 59, 999)); return { start: i, end: C }; } if (R === "this quarter") { const i = Math.floor(t / 3), C = new Date(Date.UTC(A, i * 3, 1, 0, 0, 0, 0)), a = new Date(Date.UTC(A, i * 3 + 3, 0, 23, 59, 59, 999)); return { start: C, end: a }; } if (R === "this year") { const i = new Date(Date.UTC(A, 0, 1, 0, 0, 0, 0)), C = new Date(Date.UTC(A, 11, 31, 23, 59, 59, 999)); return { start: i, end: C }; } const n = R.match(/^last\s+(\d+)\s+days?$/); if (n) { const i = parseInt(n[1], 10), C = new Date(e); C.setUTCDate(s - i + 1), C.setUTCHours(0, 0, 0, 0); const a = new Date(e); return a.setUTCHours(23, 59, 59, 999), { start: C, end: a }; } const N = R.match(/^last\s+(\d+)\s+weeks?$/); if (N) { const C = parseInt(N[1], 10) * 7, a = new Date(e); a.setUTCDate(s - C + 1), a.setUTCHours(0, 0, 0, 0); const _ = new Date(e); return _.setUTCHours(23, 59, 59, 999), { start: a, end: _ }; } if (R === "last week") { const i = S === 0 ? -13 : -6 - S, C = new Date(e); C.setUTCDate(s + i), C.setUTCHours(0, 0, 0, 0); const a = new Date(C); return a.setUTCDate(C.getUTCDate() + 6), a.setUTCHours(23, 59, 59, 999), { start: C, end: a }; } if (R === "last month") { const i = new Date(Date.UTC(A, t - 1, 1, 0, 0, 0, 0)), C = new Date(Date.UTC(A, t, 0, 23, 59, 59, 999)); return { start: i, end: C }; } if (R === "last quarter") { const i = Math.floor(t / 3), C = i === 0 ? 3 : i - 1, a = i === 0 ? A - 1 : A, _ = new Date(Date.UTC(a, C * 3, 1, 0, 0, 0, 0)), P = new Date(Date.UTC(a, C * 3 + 3, 0, 23, 59, 59, 999)); return { start: _, end: P }; } if (R === "last year") { const i = new Date(Date.UTC(A - 1, 0, 1, 0, 0, 0, 0)), C = new Date(Date.UTC(A - 1, 11, 31, 23, 59, 59, 999)); return { start: i, end: C }; } if (R === "last 12 months") { const i = new Date(Date.UTC(A, t - 11, 1, 0, 0, 0, 0)), C = new Date(e); return C.setUTCHours(23, 59, 59, 999), { start: i, end: C }; } const I = R.match(/^last\s+(\d+)\s+months?$/); if (I) { const i = parseInt(I[1], 10), C = new Date(Date.UTC(A, t - i + 1, 1, 0, 0, 0, 0)), a = new Date(e); return a.setUTCHours(23, 59, 59, 999), { start: C, end: a }; } const O = R.match(/^last\s+(\d+)\s+years?$/); if (O) { const i = parseInt(O[1], 10), C = new Date(Date.UTC(A - i, 0, 1, 0, 0, 0, 0)), a = new Date(e); return a.setUTCHours(23, 59, 59, 999), { start: C, end: a }; } return null; } /** * Normalize date values to handle strings, numbers, and Date objects * Returns ISO string for PostgreSQL/MySQL, Unix timestamp for SQLite, or null * Ensures dates are in the correct format for each database engine */ normalizeDate(E) { if (!E) return null; if (E instanceof Date) return isNaN(E.getTime()) ? null : this.databaseAdapter.isTimestampInteger() ? this.databaseAdapter.getEngineType() === "sqlite" ? Math.floor(E.getTime() / 1e3) : E.getTime() : E.toISOString(); if (typeof E == "number") { const R = E < 1e10 ? E * 1e3 : E, A = new Date(R); return isNaN(A.getTime()) ? null : this.databaseAdapter.isTimestampInteger() ? this.databaseAdapter.getEngineType() === "sqlite" ? Math.floor(R / 1e3) : R : A.toISOString(); } if (typeof E == "string") { if (/^\d{4}-\d{2}-\d{2}$/.test(E.trim())) { const A = /* @__PURE__ */ new Date(E + "T00:00:00Z"); return isNaN(A.getTime()) ? null : this.databaseAdapter.isTimestampInteger() ? this.databaseAdapter.getEngineType() === "sqlite" ? Math.floor(A.getTime() / 1e3) : A.getTime() : A.toISOString(); } const R = new Date(E); return isNaN(R.getTime()) ? null : this.databaseAdapter.isTimestampInteger() ? this.databaseAdapter.getEngineType() === "sqlite" ? Math.floor(R.getTime() / 1e3) : R.getTime() : R.toISOString(); } const e = new Date(E); return isNaN(e.getTime()) ? null : this.databaseAdapter.isTimestampInteger() ? this.databaseAdapter.getEngineType() === "sqlite" ? Math.floor(e.getTime() / 1e3) : e.getTime() : e.toISOString(); } } class pT { constructor(E, e) { this.databaseAdapter = E, this.dateTimeBuilder = e; } /** * Build filter condition using Drizzle operators */ buildFilterCondition(E, e, R, A, t) { if (t !== void 0) { if (e !== "inDateRange") throw new Error( `dateRange can only be used with 'inDateRange' operator, but got '${e}'. Use explicit date values in the 'values' array for other date operators.` ); if (A && A.type !== "time") throw new Error( `dateRange can only be used on time dimensions, but field '${A.name || "unknown"}' has type '${A.type}'` ); return this.dateTimeBuilder.buildDateRangeCondition(E, t); } if (!R || R.length === 0) return e === "equals" ? this.databaseAdapter.buildBooleanLiteral(!1) : null; const s = R.filter((n) => !(n == null || n === "" || typeof n == "string" && n.includes("\0"))).map(this.databaseAdapter.convertFilterValue); if (s.length === 0 && !["set", "notSet"].includes(e)) return e === "equals" ? this.databaseAdapter.buildBooleanLiteral(!1) : null; const S = s[0]; switch (e) { case "equals": if (s.length > 1) { if (A?.type === "time") { const n = s.map((N) => this.dateTimeBuilder.normalizeDate(N) || N); return CE(E, n); } return CE(E, s); } else if (s.length === 1) { const n = A?.type === "time" && this.dateTimeBuilder.normalizeDate(S) || S; return K(E, n); } return this.databaseAdapter.buildBooleanLiteral(!1); case "notEquals": return s.length > 1 ? Te(E, s) : s.length === 1 ? Ee(E, S) : null; case "contains": return this.databaseAdapter.buildStringCondition(E, "contains", S); case "notContains": return this.databaseAdapter.buildStringCondition(E, "notContains", S); case "startsWith": return this.databaseAdapter.buildStringCondition(E, "startsWith", S); case "endsWith": return this.databaseAdapter.buildStringCondition(E, "endsWith", S); case "gt": return LE(E, S); case "gte": return Q(E, S); case "lt": return aE(E, S); case "lte": return Z(E, S); case "set": return zE(E); case "notSet": return ee(E); case "inDateRange": if (s.length >= 2) { const n = this.dateTimeBuilder.normalizeDate(s[0]); let N = this.dateTimeBuilder.normalizeDate(s[1]); if (n && N) { const I = R[1]; if (typeof I == "string" && /^\d{4}-\d{2}-\d{2}$/.test(I.trim())) { const O = typeof N == "number" ? new Date(N * (this.databaseAdapter.getEngineType() === "sqlite" ? 1e3 : 1)) : new Date(N), i = new Date(O); i.setUTCHours(23, 59, 59, 999), this.databaseAdapter.isTimestampInteger() ? N = this.databaseAdapter.getEngineType() === "sqlite" ? Math.floor(i.getTime() / 1e3) : i.getTime() : N = i.toISOString(); } return H( Q(E, n), Z(E, N) ); } } return null; case "beforeDate": { const n = this.dateTimeBuilder.normalizeDate(S); return n ? aE(E, n) : null; } case "afterDate": { const n = this.dateTimeBuilder.normalizeDate(S); return n ? LE(E, n) : null; } case "between": return s.length >= 2 ? H( Q(E, s[0]), Z(E, s[1]) ) : null; case "notBetween": return s.length >= 2 ? eE( aE(E, s[0]), LE(E, s[1]) ) : null; case "in": return s.length > 0 ? CE(E, s) : null; case "notIn": return s.length > 0 ? Te(E, s) : null; case "like": return this.databaseAdapter.buildStringCondition(E, "like", S); case "notLike": return this.databaseAdapter.buildStringCondition(E, "notLike", S); case "ilike": return this.databaseAdapter.buildStringCondition(E, "ilike", S); case "regex": return this.databaseAdapter.buildStringCondition(E, "regex", S); case "notRegex": return this.databaseAdapter.buildStringCondition(E, "notRegex", S); case "isEmpty": return eE( ee(E), K(E, "") ); case "isNotEmpty": return H( zE(E), Ee(E, "") ); // PostgreSQL array operators - silent no-op for other databases // These use Drizzle's built-in array operator functions case "arrayContains": return this.databaseAdapter.getEngineType() === "postgres" ? rT(E, s) : null; case "arrayOverlaps": return this.databaseAdapter.getEngineType() === "postgres" ? nT(E, s) : null; case "arrayContained": return this.databaseAdapter.getEngineType() === "postgres" ? IT(E, s) : null; default: return null; } } /** * Build a logical filter (AND/OR) - used by executor for cache preloading * This handles nested filter structures and builds combined SQL */ buildLogicalFilter(E, e, R) { if ("and" in E && E.and) { const A = E.and.map((t) => this.buildSingleFilter(t, e, R)).filter((t) => t !== null); return A.length > 0 ? A.length === 1 ? A[0] : H(...A) : null; } if ("or" in E && E.or) { const A = E.or.map((t) => this.buildSingleFilter(t, e, R)).filter((t) => t !== null); return A.length > 0 ? A.length === 1 ? A[0] : eE(...A) : null; } return null; } /** * Build SQL for a single filter condition (simple or logical) * Used for cache preloading to build filters independently of query context */ buildSingleFilter(E, e, R) { if ("and" in E || "or" in E) return this.buildLogicalFilter(E, e, R); const A = E, [t, s] = A.member.split("."), S = e.get(t); if (!S) return null; const n = S.dimensions?.[s]; if (!n) return null; const N = Y(n.sql, R); return this.buildFilterCondition( N, A.operator, A.values, n, A.dateRange ); } } class $ { dependencyGraph; cubes; constructor(E) { this.cubes = E instanceof Map ? E : /* @__PURE__ */ new Map([[E.name, E]]), this.dependencyGraph = /* @__PURE__ */ new Map(); } /** * Extract {member} references from calculatedSql template * Supports both {measure} and {Cube.measure} syntax * * @param calculatedSql - Template string with {member} references * @returns Array of dependency information */ extractDependencies(E) { const e = /\{([^}]+)\}/g, R = E.matchAll(e), A = []; for (const t of R) { const s = t[1].trim(); if (s.includes(".")) { const [S, n] = s.split("."); A.push({ measureName: s, cubeName: S.trim(), fieldName: n.trim() }); } else A.push({ measureName: s, cubeName: null, fieldName: s }); } return A; } /** * Build dependency graph for all calculated measures in a cube * * @param cube - The cube containing measures */ buildGraph(E) { for (const [e, R] of Object.entries(E.measures)) if (R.type === "calculated" && R.calculatedSql) { const A = `${E.name}.${e}`, t = this.extractDependencies(R.calculatedSql), s = /* @__PURE__ */ new Set(); for (const S of t) { const N = `${S.cubeName || E.name}.${S.fieldName}`; s.add(N); } this.dependencyGraph.set(A, { id: A, dependencies: s, inDegree: 0 }); } this.calculateInDegrees(); } /** * Build dependency graph for multiple cubes * * @param cubes - Map of cubes to analyze */ buildGraphForMultipleCubes(E) { for (const e of E.values()) this.buildGraph(e); } /** * Calculate in-degree for each node (number of measures