UNPKG

drizzle-cube

Version:

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

1,694 lines (1,693 loc) 461 kB
const V = Symbol.for("drizzle:entityKind"); function d(T, E) { if (!T || typeof T != "object") return !1; if (T instanceof E) return !0; if (!Object.prototype.hasOwnProperty.call(E, V)) throw new Error( `Class "${E.name ?? "<unknown>"}" doesn't look like a Drizzle entity. If this is incorrect and the class is provided by Drizzle, please report this as a bug.` ); let e = Object.getPrototypeOf(T).constructor; if (e) for (; e; ) { if (V in e && e[V] === E[V]) return !0; e = Object.getPrototypeOf(e); } return !1; } class RE { constructor(E, e) { this.table = E, this.config = e, this.name = e.name, this.keyAsName = e.keyAsName, this.notNull = e.notNull, this.default = e.default, this.defaultFn = e.defaultFn, this.onUpdateFn = e.onUpdateFn, this.hasDefault = e.hasDefault, this.primary = e.primaryKey, this.isUnique = e.isUnique, this.uniqueName = e.uniqueName, this.uniqueType = e.uniqueType, this.dataType = e.dataType, this.columnType = e.columnType, this.generated = e.generated, this.generatedIdentity = e.generatedIdentity; } static [V] = "Column"; name; keyAsName; primary; notNull; default; defaultFn; onUpdateFn; hasDefault; isUnique; uniqueName; uniqueType; dataType; columnType; enumValues = void 0; generated = void 0; generatedIdentity = void 0; config; mapFromDriverValue(E) { return E; } mapToDriverValue(E) { return E; } // ** @internal */ shouldDisableInsert() { return this.config.generated !== void 0 && this.config.generated.type !== "byDefault"; } } const iE = Symbol.for("drizzle:Name"), TT = Symbol.for("drizzle:isPgEnum"); function ie(T) { return !!T && typeof T == "function" && TT in T && T[TT] === !0; } class xT { static [V] = "Subquery"; constructor(E, e, R, A = !1, S = []) { this._ = { brand: "Subquery", sql: E, selectedFields: e, alias: R, isWith: A, usedTables: S }; } // getSQL(): SQL<unknown> { // return new SQL([this]); // } } const ae = { startActiveSpan(T, E) { return E(); } }, TE = Symbol.for("drizzle:ViewBaseConfig"), aE = Symbol.for("drizzle:Schema"), eT = Symbol.for("drizzle:Columns"), RT = Symbol.for("drizzle:ExtraConfigColumns"), oE = Symbol.for("drizzle:OriginalName"), DE = Symbol.for("drizzle:BaseName"), sE = Symbol.for("drizzle:IsAlias"), AT = Symbol.for("drizzle:ExtraConfigBuilder"), oe = Symbol.for("drizzle:IsDrizzleTable"); class W { static [V] = "Table"; /** @internal */ static Symbol = { Name: iE, Schema: aE, OriginalName: oE, Columns: eT, ExtraConfigColumns: RT, BaseName: DE, IsAlias: sE, ExtraConfigBuilder: AT }; /** * @internal * Can be changed if the table is aliased. */ [iE]; /** * @internal * Used to store the original name of the table, before any aliasing. */ [oE]; /** @internal */ [aE]; /** @internal */ [eT]; /** @internal */ [RT]; /** * @internal * Used to store the table name before the transformation via the `tableCreator` functions. */ [DE]; /** @internal */ [sE] = !1; /** @internal */ [oe] = !0; /** @internal */ [AT] = void 0; constructor(E, e, R) { this[iE] = this[oE] = E, this[aE] = e, this[DE] = R; } } function vT(T) { return T != null && typeof T.getSQL == "function"; } function De(T) { const E = { sql: "", params: [] }; for (const e of T) E.sql += e.sql, E.params.push(...e.params), e.typings?.length && (E.typings || (E.typings = []), E.typings.push(...e.typings)); return E; } class f { static [V] = "StringChunk"; value; constructor(E) { this.value = Array.isArray(E) ? E : [E]; } getSQL() { return new H([this]); } } class H { constructor(E) { this.queryChunks = E; for (const e of E) if (d(e, W)) { const R = e[W.Symbol.Schema]; this.usedTables.push( R === void 0 ? e[W.Symbol.Name] : R + "." + e[W.Symbol.Name] ); } } static [V] = "SQL"; /** @internal */ decoder = QT; shouldInlineParams = !1; /** @internal */ usedTables = []; append(E) { return this.queryChunks.push(...E.queryChunks), this; } toQuery(E) { return ae.startActiveSpan("drizzle.buildSQL", (e) => { const R = this.buildQueryFromSourceParams(this.queryChunks, E); return e?.setAttributes({ "drizzle.query.text": R.sql, "drizzle.query.params": JSON.stringify(R.params) }), R; }); } buildQueryFromSourceParams(E, e) { const R = Object.assign({}, e, { inlineParams: e.inlineParams || this.shouldInlineParams, paramStartIndex: e.paramStartIndex || { value: 0 } }), { casing: A, escapeName: S, escapeParam: I, prepareTyping: t, inlineParams: s, paramStartIndex: O } = R; return De(E.map((N) => { if (d(N, f)) return { sql: N.value.join(""), params: [] }; if (d(N, ZE)) return { sql: S(N.value), params: [] }; if (N === void 0) return { sql: "", params: [] }; if (Array.isArray(N)) { const C = [new f("(")]; for (const [L, n] of N.entries()) C.push(n), L < N.length - 1 && C.push(new f(", ")); return C.push(new f(")")), this.buildQueryFromSourceParams(C, R); } if (d(N, H)) return this.buildQueryFromSourceParams(N.queryChunks, { ...R, inlineParams: s || N.shouldInlineParams }); if (d(N, W)) { const C = N[W.Symbol.Schema], L = N[W.Symbol.Name]; return { sql: C === void 0 || N[sE] ? S(L) : S(C) + "." + S(L), params: [] }; } if (d(N, RE)) { const C = A.getColumnCasing(N); if (e.invokeSource === "indexes") return { sql: S(C), params: [] }; const L = N.table[W.Symbol.Schema]; return { sql: N.table[sE] || L === void 0 ? S(N.table[W.Symbol.Name]) + "." + S(C) : S(L) + "." + S(N.table[W.Symbol.Name]) + "." + S(C), params: [] }; } if (d(N, qT)) { const C = N[TE].schema, L = N[TE].name; return { sql: C === void 0 || N[TE].isAlias ? S(L) : S(C) + "." + S(L), params: [] }; } if (d(N, CE)) { if (d(N.value, rE)) return { sql: I(O.value++, N), params: [N], typings: ["none"] }; const C = N.value === null ? null : N.encoder.mapToDriverValue(N.value); if (d(C, H)) return this.buildQueryFromSourceParams([C], R); if (s) return { sql: this.mapInlineParam(C, R), params: [] }; let L = ["none"]; return t && (L = [t(N.encoder)]), { sql: I(O.value++, C), params: [C], typings: L }; } return d(N, rE) ? { sql: I(O.value++, N), params: [N], typings: ["none"] } : d(N, H.Aliased) && N.fieldAlias !== void 0 ? { sql: S(N.fieldAlias), params: [] } : d(N, xT) ? N._.isWith ? { sql: S(N._.alias), params: [] } : this.buildQueryFromSourceParams([ new f("("), N._.sql, new f(") "), new ZE(N._.alias) ], R) : ie(N) ? N.schema ? { sql: S(N.schema) + "." + S(N.enumName), params: [] } : { sql: S(N.enumName), params: [] } : vT(N) ? N.shouldOmitSQLParens?.() ? this.buildQueryFromSourceParams([N.getSQL()], R) : this.buildQueryFromSourceParams([ new f("("), N.getSQL(), new f(")") ], R) : s ? { sql: this.mapInlineParam(N, R), params: [] } : { sql: I(O.value++, N), params: [N], typings: ["none"] }; })); } mapInlineParam(E, { escapeString: e }) { if (E === null) return "null"; if (typeof E == "number" || typeof E == "boolean") return E.toString(); if (typeof E == "string") return e(E); if (typeof E == "object") { const R = E.toString(); return e(R === "[object Object]" ? JSON.stringify(E) : R); } throw new Error("Unexpected param value: " + E); } getSQL() { return this; } as(E) { return E === void 0 ? this : new H.Aliased(this, E); } mapWith(E) { return this.decoder = typeof E == "function" ? { mapFromDriverValue: E } : E, this; } inlineParams() { return this.shouldInlineParams = !0, this; } /** * This method is used to conditionally include a part of the query. * * @param condition - Condition to check * @returns itself if the condition is `true`, otherwise `undefined` */ if(E) { return E ? this : void 0; } } class ZE { constructor(E) { this.value = E; } static [V] = "Name"; brand; getSQL() { return new H([this]); } } function Pe(T) { return typeof T == "object" && T !== null && "mapToDriverValue" in T && typeof T.mapToDriverValue == "function"; } const QT = { mapFromDriverValue: (T) => T }, ZT = { mapToDriverValue: (T) => T }; ({ ...QT, ...ZT }); class CE { /** * @param value - Parameter value * @param encoder - Encoder to convert the value to a driver parameter */ constructor(E, e = ZT) { this.value = E, this.encoder = e; } static [V] = "Param"; brand; getSQL() { return new H([this]); } } function r(T, ...E) { const e = []; (E.length > 0 || T.length > 0 && T[0] !== "") && e.push(new f(T[0])); for (const [R, A] of E.entries()) e.push(A, new f(T[R + 1])); return new H(e); } ((T) => { function E() { return new H([]); } T.empty = E; function e(s) { return new H(s); } T.fromList = e; function R(s) { return new H([new f(s)]); } T.raw = R; function A(s, O) { const N = []; for (const [C, L] of s.entries()) C > 0 && O !== void 0 && N.push(O), N.push(L); return new H(N); } T.join = A; function S(s) { return new ZE(s); } T.identifier = S; function I(s) { return new rE(s); } T.placeholder = I; function t(s, O) { return new CE(s, O); } T.param = t; })(r || (r = {})); ((T) => { class E { constructor(R, A) { this.sql = R, this.fieldAlias = A; } static [V] = "SQL.Aliased"; /** @internal */ isSelectionField = !1; getSQL() { return this.sql; } /** @internal */ clone() { return new E(this.sql, this.fieldAlias); } } T.Aliased = E; })(H || (H = {})); class rE { constructor(E) { this.name = E; } static [V] = "Placeholder"; getSQL() { return new H([this]); } } const Me = Symbol.for("drizzle:IsDrizzleView"); class qT { static [V] = "View"; /** @internal */ [TE]; /** @internal */ [Me] = !0; constructor({ name: E, schema: e, selectedFields: R, query: A }) { this[TE] = { name: E, originalName: E, schema: e, selectedFields: R, query: A, isExisting: !A, isAlias: !1 }; } getSQL() { return new H([this]); } } RE.prototype.getSQL = function() { return new H([this]); }; W.prototype.getSQL = function() { return new H([this]); }; xT.prototype.getSQL = function() { return new H([this]); }; function g(T, E) { return Pe(E) && !vT(T) && !d(T, CE) && !d(T, rE) && !d(T, RE) && !d(T, W) && !d(T, qT) ? new CE(T, E) : T; } const k = (T, E) => r`${T} = ${g(E, T)}`, ST = (T, E) => r`${T} <> ${g(E, T)}`; function h(...T) { const E = T.filter( (e) => e !== void 0 ); if (E.length !== 0) return E.length === 1 ? new H(E) : new H([ new f("("), r.join(E, new f(" and ")), new f(")") ]); } function PE(...T) { const E = T.filter( (e) => e !== void 0 ); if (E.length !== 0) return E.length === 1 ? new H(E) : new H([ new f("("), r.join(E, new f(" or ")), new f(")") ]); } const ME = (T, E) => r`${T} > ${g(E, T)}`, Q = (T, E) => r`${T} >= ${g(E, T)}`, UE = (T, E) => r`${T} < ${g(E, T)}`, Z = (T, E) => r`${T} <= ${g(E, T)}`; function lE(T, E) { return Array.isArray(E) ? E.length === 0 ? r`false` : r`${T} in ${E.map((e) => g(e, T))}` : r`${T} in ${g(E, T)}`; } function IT(T, E) { return Array.isArray(E) ? E.length === 0 ? r`true` : r`${T} not in ${E.map((e) => g(e, T))}` : r`${T} not in ${g(E, T)}`; } function NT(T) { return r`${T} is null`; } function OT(T) { return r`${T} is not null`; } function tT(T) { return r`${T} asc`; } function Ue(T) { return r`${T} desc`; } function cE(T) { return r`count(${T || r.raw("*")})`.mapWith(Number); } function le(T) { return r`count(distinct ${T})`.mapWith(Number); } function y(T) { return r`sum(${T})`.mapWith(String); } function AE(T) { return r`max(${T})`.mapWith(d(T, RE) ? T : String); } function SE(T) { return r`min(${T})`.mapWith(d(T, RE) ? T : String); } class jE { /** * 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 ce extends jE { 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, S) => r`${A} ${S}`); 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; } } class jT extends jE { 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, S) => r`${A} ${S}`); 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; } } class ue extends jE { 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 A = r`datetime(${e}, 'unixepoch')`; return r`datetime(strftime('%Y-%m-%d %H:00:00', ${A}))`; case "minute": const S = r`datetime(${e}, 'unixepoch')`; return r`datetime(strftime('%Y-%m-%d %H:%M:00', ${S}))`; case "second": const I = r`datetime(${e}, 'unixepoch')`; return r`datetime(strftime('%Y-%m-%d %H:%M:%S', ${I}))`; 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, S) => r`${A} ${S}`); 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; } } class Ge extends jT { 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 me(T) { switch (T) { case "postgres": return new ce(); case "mysql": return new jT(); case "sqlite": return new ue(); case "singlestore": return new Ge(); default: throw new Error(`Unsupported database engine: ${T}`); } } class kE { constructor(E, e, R) { this.db = E, this.schema = e; const A = R || this.getEngineType(); this.databaseAdapter = me(A); } databaseAdapter; } class He extends kE { async execute(E, e) { if (E && typeof E == "object" && typeof E.execute == "function") { const A = await E.execute(); return Array.isArray(A) ? A.map((S) => this.convertNumericFields(S, 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, S] of Object.entries(E)) e && e.includes(A) ? R[A] = this.coerceToNumber(S) : R[A] = S; 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 sT(T, E) { return new He(T, E, "postgres"); } class kT extends kE { async execute(E, e) { if (E && typeof E == "object" && typeof E.execute == "function") { const A = await E.execute(); return Array.isArray(A) ? A.map((S) => this.convertNumericFields(S, 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, S] of Object.entries(E)) e && e.includes(A) ? R[A] = this.coerceToNumber(S) : R[A] = S; 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 de(T, E) { return new kT(T, E, "mysql"); } class Be extends kE { 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, S] of Object.entries(E)) e && e.includes(A) ? R[A] = this.coerceToNumber(S) : R[A] = S; 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 CT(T, E) { return new Be(T, E, "sqlite"); } class pe extends kT { 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 Fe(T, E) { return new pe(T, E); } function rT(T, E, e) { if (e) switch (e) { case "postgres": return sT(T, E); case "mysql": return de(T, E); case "sqlite": return CT(T, E); case "singlestore": return Fe(T, E); } if (T.all && T.run) return CT(T, E); if (T.execute) return sT(T, E); throw new Error("Unable to determine database engine type. Please specify engineType parameter."); } function LT(T) { return typeof T == "function" ? T() : T; } function zT(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 Ye(T) { return T && typeof T == "object" ? r`${r`${T}`}` : T; } function w(T, E) { const e = typeof T == "function" ? T(E) : T; return Ye(e); } function fe(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: S } = T.through, I = []; for (const N of R) { const C = N.as || k; I.push(C(N.source, N.target)); } const t = []; for (const N of A) { const C = N.as || k; t.push(C(N.source, N.target)); } let s; if (S) { const N = S(E); s = Array.isArray(N) ? N : [N]; } const O = zT("belongsToMany", T.sqlJoinType); return { junctionJoins: [ { joinType: O, table: e, condition: h(...I) }, { joinType: O, table: e, // This will be replaced with target cube table in query planner condition: h(...t) } ], junctionSecurityConditions: s }; } class J { 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 S of R) { const I = S[1].trim(); if (I.includes(".")) { const [t, s] = I.split("."); A.push({ measureName: I, cubeName: t.trim(), fieldName: s.trim() }); } else A.push({ measureName: I, cubeName: null, fieldName: I }); } 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}`, S = this.extractDependencies(R.calculatedSql), I = /* @__PURE__ */ new Set(); for (const t of S) { const O = `${t.cubeName || E.name}.${t.fieldName}`; I.add(O); } this.dependencyGraph.set(A, { id: A, dependencies: I, 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 depending on it) */ calculateInDegrees() { for (const E of this.dependencyGraph.values()) E.inDegree = 0; for (const E of this.dependencyGraph.values()) for (const e of E.dependencies) { const R = this.dependencyGraph.get(e); R && R.inDegree++; } } /** * Perform topological sort using Kahn's algorithm * Returns measures in dependency order (dependencies first) * * @param measureNames - List of measure names to sort * @returns Sorted array of measure names * @throws Error if circular dependency detected */ topologicalSort(E) { const e = /* @__PURE__ */ new Map(), R = [], A = []; for (const S of E) { const I = this.dependencyGraph.get(S); I && e.set(S, { id: I.id, dependencies: new Set(I.dependencies), inDegree: 0 // Will recalculate below }); } for (const S of e.values()) { let I = 0; for (const t of S.dependencies) e.has(t) && I++; S.inDegree = I; } for (const [S, I] of e) I.inDegree === 0 && R.push(S); for (; R.length > 0; ) { const S = R.shift(); A.push(S); for (const [I, t] of e) t.dependencies.has(S) && (t.inDegree--, t.inDegree === 0 && R.push(I)); } if (A.length < e.size) { const S = this.detectCycle(); throw new Error( `Circular dependency detected in calculated measures: ${S ? S.join(" -> ") : "unknown cycle"}` ); } return A; } /** * Detect circular dependencies using DFS * Returns the cycle path if found, null otherwise * * @returns Array representing the cycle, or null */ detectCycle() { const E = /* @__PURE__ */ new Set(), e = /* @__PURE__ */ new Set(), R = []; for (const A of this.dependencyGraph.keys()) if (!E.has(A)) { const S = this.dfs(A, E, e, R); if (S) return S; } return null; } /** * DFS helper for cycle detection */ dfs(E, e, R, A) { e.add(E), R.add(E), A.push(E); const S = this.dependencyGraph.get(E); if (!S) return A.pop(), R.delete(E), null; for (const I of S.dependencies) if (e.has(I)) { if (R.has(I)) { const t = A.indexOf(I); return [...A.slice(t), I]; } } else { const t = this.dfs(I, e, R, A); if (t) return t; } return A.pop(), R.delete(E), null; } /** * Get all dependencies for a specific measure (direct and transitive) * * @param measureName - Full measure name (e.g., "Cube.measure") * @returns Set of all dependency measure names */ getAllDependencies(E) { const e = /* @__PURE__ */ new Set(), R = /* @__PURE__ */ new Set(), A = (S) => { if (R.has(S)) return; R.add(S); const I = this.dependencyGraph.get(S); if (I) for (const t of I.dependencies) e.add(t), A(t); }; return A(E), e; } /** * Validate that all dependencies exist * * @param cube - The cube to validate * @throws Error if dependencies are missing */ validateDependencies(E) { for (const [e, R] of Object.entries(E.measures)) if (R.type === "calculated" && R.calculatedSql) { const A = this.extractDependencies(R.calculatedSql); for (const S of A) { const I = S.cubeName || E.name, t = this.cubes.get(I); if (!t) throw new Error( `Calculated measure '${E.name}.${e}' references unknown cube '${I}'` ); if (!t.measures[S.fieldName]) throw new Error( `Calculated measure '${E.name}.${e}' references unknown measure '${S.measureName}'` ); if (I === E.name && S.fieldName === e) throw new Error( `Calculated measure '${E.name}.${e}' cannot reference itself` ); } } } /** * Auto-populate dependencies array for calculated measures * Updates the measure objects with detected dependencies * * @param cube - The cube to update */ populateDependencies(E) { for (const [, e] of Object.entries(E.measures)) if (e.type === "calculated" && e.calculatedSql && !e.dependencies) { const R = this.extractDependencies(e.calculatedSql); e.dependencies = R.map((A) => A.measureName); } } /** * Check if a measure is a calculated measure * * @param measure - The measure to check * @returns True if the measure is calculated */ static isCalculatedMeasure(E) { return E.type === "calculated" && !!E.calculatedSql; } } function he(T, E) { const { cube: e, allCubes: R, resolvedMeasures: A } = E, S = zE(T), I = /* @__PURE__ */ new Map(); for (const C of S) { const { originalRef: L, cubeName: n, fieldName: i } = C, o = n || e.name; if (!R.get(o)) throw new Error( `Cannot substitute {${L}}: cube '${o}' not found` ); const P = `${o}.${i}`, l = A.get(P); if (!l) throw new Error( `Cannot substitute {${L}}: measure '${P}' not resolved yet. Ensure measures are resolved in dependency order.` ); const M = l(), G = r`${M}`; I.set(L, G); } const t = [], s = []; let O = 0; for (const C of S) { const L = `{${C.originalRef}}`, n = T.indexOf(L, O); if (n >= 0) { t.push(T.substring(O, n)); const i = I.get(C.originalRef); i && s.push(i), O = n + L.length; } } if (t.push(T.substring(O)), s.length === 0) return r.raw(T); const N = []; for (let C = 0; C < t.length; C++) t[C] && N.push(new f(t[C])), C < s.length && N.push(s[C]); return r.join(N, r.raw("")); } function zE(T) { const E = /\{([^}]+)\}/g, e = T.matchAll(E), R = []; for (const A of e) { const S = A[1].trim(); if (S.includes(".")) { const [I, t] = S.split(".").map((s) => s.trim()); R.push({ originalRef: S, cubeName: I, fieldName: t }); } else R.push({ originalRef: S, cubeName: null, fieldName: S }); } return R; } function Ve(T) { const E = []; let e = 0; for (let I = 0; I < T.length; I++) if (T[I] === "{") e++; else if (T[I] === "}" && (e--, e < 0)) { E.push(`Unmatched closing brace at position ${I}`); break; } e > 0 && E.push("Unmatched opening brace in template"), /\{\s*\}/.test(T) && E.push("Empty member reference {} found in template"), /\{[^}]*\{/.test(T) && E.push("Nested braces are not allowed in member references"); const S = zE(T); for (const I of S) { const t = I.cubeName ? `${I.cubeName}.${I.fieldName}` : I.fieldName; /^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(t) || E.push( `Invalid member reference {${I.originalRef}}: must start with letter or underscore, and contain only letters, numbers, underscores, and dots` ), t.split(".").length > 2 && E.push( `Invalid member reference {${I.originalRef}}: only one dot allowed (Cube.measure format)` ); } return { isValid: E.length === 0, errors: E }; } function uE(T, E) { const e = zE(T), R = /* @__PURE__ */ new Set(); for (const A of e) { const I = `${A.cubeName || E}.${A.fieldName}`; R.add(I); } return Array.from(R); } class We { constructor(E) { this.databaseAdapter = E; } /** * Build resolvedMeasures map for a set of measures * This centralizes the logic for building both regular and calculated measures * in dependency order, avoiding duplication across main queries and CTEs * * @param measureNames - Array of measure names to resolve (e.g., ["Employees.count", "Employees.activePercentage"]) * @param cubeMap - Map of all cubes involved in the query * @param context - Query context with database and security context * @param customMeasureBuilder - Optional function to override how individual measures are built * @returns Map of measure names to SQL builder functions */ buildResolvedMeasures(E, e, R, A) { const S = /* @__PURE__ */ new Map(), I = [], t = [], s = new Set(E), O = new J(e); for (const N of e.values()) O.buildGraph(N); for (const N of E) { const [C, L] = N.split("."), n = e.get(C); if (n && n.measures && n.measures[L]) { const i = n.measures[L]; J.isCalculatedMeasure(i) ? (t.push(N), uE(i.calculatedSql, C).forEach((P) => s.add(P)), O.getAllDependencies(N).forEach((P) => { const [l, M] = P.split("."), G = e.get(l); if (G && G.measures[M]) { const m = G.measures[M]; J.isCalculatedMeasure(m) && uE(m.calculatedSql, l).forEach((b) => s.add(b)); } })) : I.push(N); } } for (const N of s) { const [C, L] = N.split("."), n = e.get(C); if (n && n.measures && n.measures[L]) { const i = n.measures[L]; J.isCalculatedMeasure(i) ? t.includes(N) || t.push(N) : I.includes(N) || I.push(N); } } for (const N of I) { const [C, L] = N.split("."), n = e.get(C), i = n.measures[L]; if (A) { const o = A(N, i, n); S.set(N, () => o); } else S.set(N, () => this.buildMeasureExpression(i, R)); } if (t.length > 0) { const N = O.topologicalSort(t); for (const C of N) { const [L, n] = C.split("."), i = e.get(L), o = i.measures[n]; S.set(C, () => this.buildCalculatedMeasure( o, i, e, S, R )); } } return S; } /** * Build dynamic selections for measures, dimensions, and time dimensions * Works for both single and multi-cube queries * Handles calculated measures with dependency resolution */ buildSelections(E, e, R) { const A = {}, S = E instanceof Map ? E : /* @__PURE__ */ new Map([[E.name, E]]); if (e.dimensions) for (const I of e.dimensions) { const [t, s] = I.split("."), O = S.get(t); if (O && O.dimensions && O.dimensions[s]) { const N = O.dimensions[s], C = w(N.sql, R); A[I] = r`${C}`.as(I); } } if (e.measures) { const I = this.buildResolvedMeasures( e.measures, S, R ); for (const t of e.measures) { const s = I.get(t); if (s) { const O = s(); A[t] = r`${O}`.as(t); } } } if (e.timeDimensions) for (const I of e.timeDimensions) { const [t, s] = I.dimension.split("."), O = S.get(t); if (O && O.dimensions && O.dimensions[s]) { const N = O.dimensions[s], C = this.buildTimeDimensionExpression( N.sql, I.granularity, R ); A[I.dimension] = r`${C}`.as(I.dimension); } } return Object.keys(A).length === 0 && (A.count = cE()), A; } /** * Build calculated measure expression by substituting {member} references * with resolved SQL expressions */ buildCalculatedMeasure(E, e, R, A, S) { if (!E.calculatedSql) throw new Error( `Calculated measure '${e.name}.${E.name}' missing calculatedSql property` ); const I = this.databaseAdapter.preprocessCalculatedTemplate(E.calculatedSql); return he(I, { cube: e, allCubes: R, resolvedMeasures: A }); } /** * Build resolved measures map for a calculated measure from CTE columns * This handles re-aggregating pre-aggregated CTE columns for calculated measures * * IMPORTANT: For calculated measures in CTEs, we cannot sum/avg pre-computed ratios. * We must recalculate from the base measures that were pre-aggregated in the CTE. * * @param measure - The calculated measure to build * @param cube - The cube containing this measure * @param cteInfo - CTE metadata (alias, measures, cube reference) * @param allCubes - Map of all cubes in the query * @param context - Query context * @returns SQL expression for the calculated measure using CTE column references */ buildCTECalculatedMeasure(E, e, R, A, S) { if (!E.calculatedSql) throw new Error( `Calculated measure '${e.name}.${E.name || "unknown"}' missing calculatedSql property` ); const I = /* @__PURE__ */ new Map(), t = uE(E.calculatedSql, e.name); for (const s of t) { const [O, N] = s.split("."), C = A.get(O); if (C && C.measures[N]) { const L = C.measures[N]; if (R.measures.includes(s)) { const n = r`${r.identifier(R.cteAlias)}.${r.identifier(N)}`; let i; switch (L.type) { case "count": case "countDistinct": case "sum": i = y(n); break; case "avg": i = this.databaseAdapter.buildAvg(n); break; case "min": i = SE(n); break; case "max": i = AE(n); break; case "number": i = y(n); break; default: i = y(n); } I.set(s, () => i); } } } return this.buildCalculatedMeasure( E, e, A, I, S ); } /** * Build measure expression for HAVING clause, handling CTE references correctly */ buildHavingMeasureExpression(E, e, R, A, S) { if (S && S.preAggregationCTEs) { const I = S.preAggregationCTEs.find((t) => t.cube.name === E); if (I && I.measures.includes(`${E}.${e}`)) if (R.type === "calculated" && R.calculatedSql) { const t = S.primaryCube.name === E ? S.primaryCube : S.joinCubes?.find((O) => O.cube.name === E)?.cube; if (!t) throw new Error(`Cube ${E} not found in query plan`); const s = /* @__PURE__ */ new Map([[S.primaryCube.name, S.primaryCube]]); if (S.joinCubes) for (const O of S.joinCubes) s.set(O.cube.name, O.cube); return this.buildCTECalculatedMeasure( R, t, I, s, A ); } else { const t = r`${r.identifier(I.cteAlias)}.${r.identifier(e)}`; switch (R.type) { case "count": case "countDistinct": case "sum": return y(t); case "avg": return this.databaseAdapter.buildAvg(t);