From 129101829f6794e7777c41038c52f0fba7619336 Mon Sep 17 00:00:00 2001 From: Pavel Tiunov Date: Wed, 26 Aug 2026 14:05:33 -0700 Subject: [PATCH 1/5] docs: view-level measures and multi-fact derived metrics, plus a schema-compiler test (#11656) * test(schema-compiler): cover multi-fact derived measure defined on a view Adds a Tesseract test for the AOV basket shape: a ratio measure authored on a view whose numerator (sum of sales amount) and denominator (count distinct of transaction ids, narrowed by filters owned by the line-item cube) live in two fact cubes at different grains, joined only through shared items, locations and dates cubes. Covers the working shape - a multi_stage view measure, whose division is evaluated after each fact has been aggregated to the query grain - and pins the current limits: the same expression without multi_stage, and any multi-fact query carrying a segment, both fail to find a join path, and the legacy planner does not plan multi-fact queries at all. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RLzMjTkAfG9tjDmjVR3EL2 * docs: document view-level measures and multi-fact derived metrics Views can define their own measures and dimensions as long as the SQL only combines members the view already includes. Adds the `measures` and `dimensions` parameters to the view reference, and a short section on the views concept page for the case they exist for: a metric whose parts live in different cubes. Adds a "Combining facts in one measure" section to multi-fact views covering `multi_stage`, which defers the expression until each fact has been aggregated and joined - without it the same measure fails to find a join path across the two facts. Adds an average order value recipe covering both shapes: a plain calculated measure when both parts sit in one cube, and a multi_stage view measure when revenue and transaction count come from two fact tables at different grains. Also corrects the multi-fact filters section: fact-specific filters and segments are not applied per subquery, they fail the query - a measure's own `filters` is what narrows one fact. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RLzMjTkAfG9tjDmjVR3EL2 * docs: reconcile the two multi-fact filter sections The SQL API "Filtering the join" bullet claimed a predicate on a fact-specific dimension filters only that fact's subquery, which contradicts the corrected "Filters and segments" section. The rewrite does attach the predicate to that fact's member, but the merged query is then planned like any other multi-fact query, so the member still has to be shared by all facts. Says that once, and points the SQL API bullet at it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RLzMjTkAfG9tjDmjVR3EL2 * docs: mark cross-cube view measures multi_stage; test the fan-out case The orders/line_items example was wrong: the two cubes join one_to_many, so a plain calculated view measure is evaluated inside the single joined scan and its `sum` runs over the multiplied rows - 400 instead of 300 on the sample data, a silently inflated numerator. `multi_stage` aggregates each side first. Makes the rule one rule on all pages: a view measure combining members of different cubes wants `multi_stage`, whether the cubes join with fan-out (wrong number) or don't join at all (no join path). The guide page now shows the short version and links the reference instead of repeating it. Test changes: - covers both reference forms - `{CUBE.member}` and `{view_name.member}` - and pins that a bare `{member}` is rejected - new fan-out block: the plain measure inlines into the multiplied join, the multi_stage one aggregates each side before dividing - drops the planner-generated `q_N` aliases from the ratio pattern, keeping the assertion on the aggregate columns - compiles the model once per suite, and splits the positive segment case out of the negative test Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RLzMjTkAfG9tjDmjVR3EL2 * test: temper the fan-out assertion; match the docs example to it The `[\s\S]*?` span between the sum and its GROUP BY was unanchored, so it would happily cross a `LEFT JOIN "line_items"` and land on a later GROUP BY - passing on exactly the regression it exists to catch. Tempered against `line_items`; checked it still matches the real multi_stage SQL and now rejects a doctored copy whose sum leg carries the join. The docs example now nests `orders.line_items` and says the two cubes join one_to_many, so the view block on its own shows which of the two cases the surrounding prose is describing. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RLzMjTkAfG9tjDmjVR3EL2 * docs: state the join relationship in prose, not inside the YAML tab A CodeGroup shows one tab at a time, so a note added as a YAML comment is invisible to a reader on the JavaScript tab - and the relationship is exactly what the example needs stated, since `orders.line_items` alone doesn't say whether the join fans out. Moved above the CodeGroup on both pages. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RLzMjTkAfG9tjDmjVR3EL2 * docs: trim the duplicated view-measure example on the guide page The block was byte-identical to the one on the view reference, against docs-mintlify/CLAUDE.md's "say it once". The guide page's point is that a view can declare a measure at all, so it now shows just that declaration, elides the cubes block, and links the reference for the full example. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RLzMjTkAfG9tjDmjVR3EL2 --------- Co-authored-by: Claude --- docs-mintlify/docs.json | 1 + .../docs/data-modeling/multi-fact-views.mdx | 136 +++++- docs-mintlify/docs/data-modeling/views.mdx | 49 ++ .../data-modeling/average-order-value.mdx | 338 ++++++++++++++ .../reference/data-modeling/view.mdx | 121 +++++ ...multi-fact-derived-measure-in-view.test.ts | 430 ++++++++++++++++++ 6 files changed, 1066 insertions(+), 9 deletions(-) create mode 100644 docs-mintlify/recipes/data-modeling/average-order-value.mdx create mode 100644 packages/cubejs-schema-compiler/test/unit/multi-fact-derived-measure-in-view.test.ts diff --git a/docs-mintlify/docs.json b/docs-mintlify/docs.json index 2002bbbc437b5..90bed16172475 100644 --- a/docs-mintlify/docs.json +++ b/docs-mintlify/docs.json @@ -640,6 +640,7 @@ "recipes/data-modeling/nested-aggregates", "recipes/data-modeling/filtered-aggregates", "recipes/data-modeling/share-of-total", + "recipes/data-modeling/average-order-value", "recipes/data-modeling/period-over-period" ] }, diff --git a/docs-mintlify/docs/data-modeling/multi-fact-views.mdx b/docs-mintlify/docs/data-modeling/multi-fact-views.mdx index 4b7d5a6fd19f5..0382ada44af6f 100644 --- a/docs-mintlify/docs/data-modeling/multi-fact-views.mdx +++ b/docs-mintlify/docs/data-modeling/multi-fact-views.mdx @@ -308,6 +308,102 @@ The combined result shows measures from each fact table side by side: Charlie has no orders and Diana has no returns — both are still included with `NULL` values for the missing fact table. +## Combining facts in one measure + +Putting measures from two facts side by side is often not the goal — you want a +single metric derived from both, such as revenue per order where revenue and +order count come from different fact tables. Neither cube can define it, because +neither can reference the other's measures. + +Define it as a [measure of the view][ref-view-measures] instead, and mark it +[`multi_stage`][ref-multi-stage]: + + + +```yaml title="YAML" +views: + - name: customer_overview + cubes: + - join_path: orders + prefix: true + includes: + - count + - total_amount + - join_path: returns + prefix: true + includes: + - total_refund + - join_path: customers + includes: + - name + - city + - join_path: dates + includes: + - date + + measures: + - name: refund_rate + type: number + multi_stage: true + sql: "{CUBE.returns_total_refund} / NULLIF({CUBE.orders_total_amount}, 0)" +``` + +```javascript title="JavaScript" +view(`customer_overview`, { + cubes: [ + { + join_path: orders, + prefix: true, + includes: [`count`, `total_amount`] + }, + { + join_path: returns, + prefix: true, + includes: [`total_refund`] + }, + { + join_path: customers, + includes: [`name`, `city`] + }, + { + join_path: dates, + includes: [`date`] + } + ], + + measures: { + refund_rate: { + type: `number`, + multi_stage: true, + sql: `${CUBE.returns_total_refund} / NULLIF(${CUBE.orders_total_amount}, 0)` + } + } +}) +``` + + + +`multi_stage` is what makes this work. It defers the expression to a stage that +runs _after_ the per-fact subqueries have been aggregated and joined, so the +division happens once per row of the combined result: + +```sql +-- one aggregating subquery per fact, at the query's grain +SUM(orders.amount) GROUP BY city +SUM(returns.refund) GROUP BY city +-- final stage, once the two are joined on city +total_refund / NULLIF(total_amount, 0) +``` + +Without `multi_stage`, the same expression is planned as an ordinary calculated +measure. Cube then looks for a single join tree covering both fact cubes, finds +none — the facts only meet through the shared dimensions — and the query fails +with `Can't find join path to join …`, naming both facts. If you see that error +on a measure that spans facts, `multi_stage` is what's missing. + +The measure is queried like any other, on its own or next to its components, and +grouped by any of the shared dimensions. + ## Joining views in the SQL API You don't have to define a dedicated multi-fact view to get multi-fact @@ -407,17 +503,20 @@ GROUP BY 1, 2 ### Filtering the join -Filters on top of the join are supported and are applied to the merged query: +Filters on top of the join are pushed into the merged query: -- A `WHERE` clause is pushed into the merged scan. A predicate on a dimension - shared by all facts filters the whole result; a predicate on a fact-specific - dimension filters only that fact's subquery. +- A `WHERE` clause is pushed into the merged scan, becoming a filter on the + member the predicate refers to. - A predicate in the `ON` clause that the planner can attach to a single side (for example, a condition on the optional side of a `LEFT JOIN`) becomes a filter on that fact. Predicates that the SQL planner can't push to one side of an outer join (such as a left-table condition in a `LEFT JOIN ON`) aren't supported by the planner and will raise an error. +Pushing the predicate in is only the first step: the merged query is then +planned like any other multi-fact query, so the member it filters on must be +[shared by all facts](#filters-and-segments). + ### Join type The facts are stitched together with a `FULL JOIN` on the shared key, and the @@ -518,14 +617,29 @@ the multi-fact join on the full set of common dimensions. **Common dimension filters** (like `city = 'New York'` or `date > '2025-01-01'`) are applied to every subquery, ensuring consistent filtering across all facts. -**Fact-specific filters** (like `orders.status = 'completed'`) are applied only -to that fact's subquery. Other fact subqueries remain unaffected. - **Measure filters** (like `orders_count > 1`) are applied as `HAVING` conditions after the subqueries are joined. -[Segments][ref-segments] that belong to a specific fact table are applied only -to that fact's subquery. +**Fact-specific filters and [segments][ref-segments]** — anything that belongs to +one fact table rather than a shared dimension — can't be used in a multi-fact +query, however it is written: a `WHERE` clause in the SQL API, a filter in the +REST (JSON) API, or a segment. Every grouped dimension, filter and segment has +to be reachable from all facts, so a query that carries one fails with +`Can't find join path to join …`. The same members are fine as soon as only +that fact's measures are requested, since the query is no longer multi-fact. + +To narrow one fact inside a multi-fact query, put the condition in the measure's +own [`filters`][ref-measure-filters] on its cube. It travels with the measure +into that fact's subquery and leaves the others alone: + +```yaml +measures: + - name: completed_amount + sql: amount + type: sum + filters: + - sql: "{CUBE}.status = 'completed'" +``` ## Join path requirements @@ -533,9 +647,13 @@ to that fact's subquery. - Dimension tables should be included in the view at **root-level join paths**, not nested under a specific fact (e.g., `customers`, not `orders.customers`) - Use `prefix` on fact cubes to disambiguate identically named members +- Everything a multi-fact query groups or filters by must be shared by all facts [ref-views]: /docs/data-modeling/views [ref-view-ref]: /reference/data-modeling/view [ref-segments]: /reference/data-modeling/segments +[ref-measure-filters]: /reference/data-modeling/measures#filters +[ref-multi-stage]: /reference/data-modeling/measures#multi_stage +[ref-view-measures]: /reference/data-modeling/view#measures [ref-sql-api]: /reference/core-data-apis/sql-api [link-tesseract]: https://cube.dev/blog/introducing-tesseract diff --git a/docs-mintlify/docs/data-modeling/views.mdx b/docs-mintlify/docs/data-modeling/views.mdx index f4ecd2cf17892..658952f49d545 100644 --- a/docs-mintlify/docs/data-modeling/views.mdx +++ b/docs-mintlify/docs/data-modeling/views.mdx @@ -283,6 +283,51 @@ control which members are exposed, how they're named, and how they're organized. This keeps your model [DRY][wiki-dry] and makes maintenance straightforward. +### Define a metric on a view when it spans cubes + +The one exception is a metric whose parts live in different cubes, so there is +no single cube it could belong to. A view can define its own +[measures][ref-view-measures] and [dimensions][ref-view-dimensions] as long as +their `sql` only combines members the view already includes — a member that +reads a column instead is rejected at compile time: + + + +```yaml title="YAML" +views: + - name: orders_overview + # cubes: … includes orders.total_amount and line_items.count + + measures: + - name: average_line_value + type: number + multi_stage: true + sql: "{CUBE.total_amount} / NULLIF({CUBE.count}, 0)" +``` + +```javascript title="JavaScript" +view(`orders_overview`, { + // cubes: … includes orders.total_amount and line_items.count + + measures: { + average_line_value: { + type: `number`, + multi_stage: true, + sql: `${CUBE.total_amount} / NULLIF(${CUBE.count}, 0)` + } + } +}) +``` + + + +[`multi_stage`][ref-multi-stage] matters whenever the parts come from different +cubes: it aggregates each of them before combining, instead of evaluating the +expression inside one joined scan where a `one_to_many` join between them would +inflate the numerator. If the cubes don't join to each other at all, see +[multi-fact views][ref-multi-fact-views]. The full example, with the `cubes` +block, is on the [view reference][ref-view-measures]. + ### Control visibility Not every view should be publicly accessible. Use [`public`][ref-view-public] @@ -450,6 +495,10 @@ parameters. [ref-view-description]: /reference/data-modeling/view#description [ref-view-title]: /reference/data-modeling/view#title [ref-view-public]: /reference/data-modeling/view#public +[ref-view-measures]: /reference/data-modeling/view#measures +[ref-view-dimensions]: /reference/data-modeling/view#dimensions +[ref-multi-stage]: /reference/data-modeling/measures#multi_stage +[ref-multi-fact-views]: /docs/data-modeling/multi-fact-views [ref-view-folders]: /reference/data-modeling/view#folders [ref-access-policies]: /reference/data-modeling/data-access-policies [ref-ai-context]: /docs/data-modeling/ai-context diff --git a/docs-mintlify/recipes/data-modeling/average-order-value.mdx b/docs-mintlify/recipes/data-modeling/average-order-value.mdx new file mode 100644 index 0000000000000..dc5e7281b9a09 --- /dev/null +++ b/docs-mintlify/recipes/data-modeling/average-order-value.mdx @@ -0,0 +1,338 @@ +--- +title: Calculating average order value +description: Define AOV as a single measure when its numerator and denominator live in the same cube, or in two fact tables at different grains. +--- + +## Use case + +Average order value (AOV) — sometimes called basket size — is revenue divided by +the number of orders. It looks like a one-line calculation, but where the two +parts live decides how it is modeled: + +- **[Same cube](#same-cube)** — both parts are measures of one fact table. +- **[Two fact tables](#across-two-fact-tables)** — revenue is aggregated at one + grain (say, day/item/location) and orders are counted at another (transaction + lines). This is the common shape in retail models. + +In both cases AOV is a ratio of two aggregates, so it must be computed _after_ +its parts are aggregated — never as a row-level `amount / orders` expression. + +## Same cube + +When both parts are measures of the same cube, define AOV as a calculated +measure that divides them: + + + +```yaml title="YAML" +cubes: + - name: orders + sql_table: orders + + dimensions: + - name: id + sql: id + type: number + primary_key: true + + measures: + - name: revenue + sql: amount + type: sum + format: currency + + - name: count + type: count + + - name: average_order_value + sql: "{revenue} / NULLIF({count}, 0)" + type: number + format: currency +``` + +```javascript title="JavaScript" +cube(`orders`, { + sql_table: `orders`, + + dimensions: { + id: { sql: `id`, type: `number`, primary_key: true } + }, + + measures: { + revenue: { sql: `amount`, type: `sum`, format: `currency` }, + count: { type: `count` }, + + average_order_value: { + sql: `${revenue} / NULLIF(${count}, 0)`, + type: `number`, + format: `currency` + } + } +}) +``` + + + +`NULLIF` guards the division so a group with no orders returns `NULL` rather +than failing. + +## Across two fact tables + +Retail models usually split the two parts. Sales dollars come from a +pre-aggregated daily table (`item_location_sales`, one row per day, item and +location), while the transaction count comes from the line-item table +(`sales_line_item`, one row per transaction line). The two never join to each +other — they meet through shared `items`, `locations` and `dates` cubes, which +makes this a [multi-fact query][ref-multi-fact-views]. + + + +Multi-fact views and multi-stage measures are powered by Tesseract, the +[next-generation data modeling engine][link-tesseract]. In versions before +v1.7.0, it was not enabled by default. + + + +### 1. Define each part on the cube that owns it + +The denominator counts distinct transactions and excludes exchanges and +non-store channels. Write that logic once, as measure +[`filters`][ref-measure-filters] on the line-item cube, so every consumer picks +it up by including the measure — never restate it per view: + + + +```yaml title="YAML" +cubes: + - name: sales_line_item + sql_table: sales_line_item + + joins: + - name: items + sql: "{CUBE}.item_id = {items.id}" + relationship: many_to_one + - name: locations + sql: "{CUBE}.location_id = {locations.id}" + relationship: many_to_one + - name: dates + sql: "DATE_TRUNC('day', {CUBE}.sold_at) = {dates.date}" + relationship: many_to_one + + dimensions: + - name: id + sql: id + type: number + primary_key: true + + measures: + - name: transactions_without_returns + sql: transaction_id + type: count_distinct + filters: + - sql: "{CUBE}.transaction_type <> 'EXCHANGE'" + - sql: "{CUBE}.fulfillment_channel_group IN ('IN_STORE', 'SHIP_FROM_STORE')" + + - name: item_location_sales + sql_table: item_location_sales + + joins: + - name: items + sql: "{CUBE}.item_id = {items.id}" + relationship: many_to_one + - name: locations + sql: "{CUBE}.location_id = {locations.id}" + relationship: many_to_one + - name: dates + sql: "DATE_TRUNC('day', {CUBE}.date) = {dates.date}" + relationship: many_to_one + + dimensions: + - name: id + sql: id + type: number + primary_key: true + + measures: + - name: sales_amount + sql: sales_amount + type: sum + format: currency +``` + +```javascript title="JavaScript" +cube(`sales_line_item`, { + sql_table: `sales_line_item`, + + joins: { + items: { + sql: `${CUBE}.item_id = ${items.id}`, + relationship: `many_to_one` + }, + locations: { + sql: `${CUBE}.location_id = ${locations.id}`, + relationship: `many_to_one` + }, + dates: { + sql: `DATE_TRUNC('day', ${CUBE}.sold_at) = ${dates.date}`, + relationship: `many_to_one` + } + }, + + dimensions: { + id: { sql: `id`, type: `number`, primary_key: true } + }, + + measures: { + transactions_without_returns: { + sql: `transaction_id`, + type: `count_distinct`, + filters: [ + { sql: `${CUBE}.transaction_type <> 'EXCHANGE'` }, + { sql: `${CUBE}.fulfillment_channel_group IN ('IN_STORE', 'SHIP_FROM_STORE')` } + ] + } + } +}) + +cube(`item_location_sales`, { + sql_table: `item_location_sales`, + + joins: { + items: { + sql: `${CUBE}.item_id = ${items.id}`, + relationship: `many_to_one` + }, + locations: { + sql: `${CUBE}.location_id = ${locations.id}`, + relationship: `many_to_one` + }, + dates: { + sql: `DATE_TRUNC('day', ${CUBE}.date) = ${dates.date}`, + relationship: `many_to_one` + } + }, + + dimensions: { + id: { sql: `id`, type: `number`, primary_key: true } + }, + + measures: { + sales_amount: { sql: `sales_amount`, type: `sum`, format: `currency` } + } +}) +``` + + + +Both facts join to the same `items`, `locations` and `dates` cubes. The `dates` +spine matters: without it the two facts have no common time member to group by, +since one is keyed by day and the other by timestamp. + +### 2. Define AOV on the view + +Neither cube can define AOV — neither can reference the other's measures. Define +it as a [measure of the view][ref-view-measures] and mark it +[`multi_stage`][ref-multi-stage]: + + + +```yaml title="YAML" +views: + - name: retail_analysis + cubes: + - join_path: item_location_sales + includes: + - sales_amount + - join_path: sales_line_item + includes: + - transactions_without_returns + - join_path: dates + includes: + - date + - join_path: items + includes: + - department + - join_path: locations + includes: + - region + + measures: + - name: aov_basket + type: number + format: currency + multi_stage: true + sql: "{CUBE.sales_amount} / NULLIF({CUBE.transactions_without_returns}, 0)" +``` + +```javascript title="JavaScript" +view(`retail_analysis`, { + cubes: [ + { + join_path: item_location_sales, + includes: [`sales_amount`] + }, + { + join_path: sales_line_item, + includes: [`transactions_without_returns`] + }, + { + join_path: dates, + includes: [`date`] + }, + { + join_path: items, + includes: [`department`] + }, + { + join_path: locations, + includes: [`region`] + } + ], + + measures: { + aov_basket: { + type: `number`, + format: `currency`, + multi_stage: true, + sql: `${CUBE.sales_amount} / NULLIF(${CUBE.transactions_without_returns}, 0)` + } + } +}) +``` + + + +The shared dimension cubes sit at root-level join paths, so `date`, `department` +and `region` are common to both facts and can be grouped by. + +### 3. Query it + +Querying `aov_basket` by `region` aggregates each fact on its own, stitches the +two results on the shared dimension, and takes the division over the joined rows: + +```sql +-- one aggregating subquery per fact, at the query's grain +SUM(item_location_sales.sales_amount) GROUP BY region +COUNT(DISTINCT CASE WHEN … THEN transaction_id END) GROUP BY region +-- final stage, once the two are joined on region +sales_amount / NULLIF(transactions_without_returns, 0) +``` + +The measure filters travel into the line-item subquery, so the exchange and +channel rules are applied exactly where they were defined. + + + +`multi_stage: true` is what defers the division until both facts have been +aggregated. Without it, Cube plans the expression as an ordinary calculated +measure, looks for a single join tree covering both fact cubes, and fails with +`Can't find join path to join 'locations', 'item_location_sales', +'sales_line_item'`. + + + +[ref-multi-fact-views]: /docs/data-modeling/multi-fact-views +[ref-multi-stage]: /reference/data-modeling/measures#multi_stage +[ref-measure-filters]: /reference/data-modeling/measures#filters +[ref-view-measures]: /reference/data-modeling/view#measures +[link-tesseract]: https://cube.dev/blog/introducing-tesseract diff --git a/docs-mintlify/reference/data-modeling/view.mdx b/docs-mintlify/reference/data-modeling/view.mdx index 37307e0aa66a1..eef38521afe3c 100644 --- a/docs-mintlify/reference/data-modeling/view.mdx +++ b/docs-mintlify/reference/data-modeling/view.mdx @@ -566,6 +566,125 @@ If you'd like to override the [format][ref-dim-format] of a member, you can use If you'd like to override the [metadata][ref-dim-meta] of a member, you can use the `meta` parameter. Note that the `meta` is overridded as a whole. +### `measures` + +The `measures` parameter defines measures on the view itself. A view measure is +always _derived_: its `sql` may only reference members that the view already +includes, never columns of a table. Use it for a metric whose parts come from +different cubes, which therefore has no single cube to live in. + +Reference the included members as `{CUBE.member}` (or `{view_name.member}`); a +bare `{member}` does not resolve inside a view. + +In the example below, `orders` joins `line_items` `one_to_many`: + + + +```yaml title="YAML" +views: + - name: orders_overview + cubes: + - join_path: orders + includes: + - total_amount + - join_path: orders.line_items + includes: + - count + + measures: + - name: average_line_value + type: number + multi_stage: true + sql: "{CUBE.total_amount} / NULLIF({CUBE.count}, 0)" +``` + +```javascript title="JavaScript" +view(`orders_overview`, { + cubes: [ + { + join_path: orders, + includes: [`total_amount`] + }, + { + join_path: orders.line_items, + includes: [`count`] + } + ], + + measures: { + average_line_value: { + type: `number`, + multi_stage: true, + sql: `${CUBE.total_amount} / NULLIF(${CUBE.count}, 0)` + } + } +}) +``` + + + +A view measure that owns its SQL — anything that reads a column rather than a +member — is rejected at compile time with `View 'orders_overview' defines own +member 'orders_overview.average_line_value'`. Move that definition to a cube and +include it instead. + +Mark a measure that combines members of **different cubes** +[`multi_stage`][ref-ref-multi-stage], as above. Without it the expression is +evaluated inside a single joined scan, which is only correct when nothing in it +is affected by the join: + +- Across cubes joined `one_to_many`, an aggregate on the one side is taken over + the rows the join multiplied, so the result is silently inflated. +- Across cubes that don't join to each other at all, there is no single join + tree to evaluate it in and the query fails with `Can't find join path to + join …`. See [multi-fact views][ref-multi-fact-views]. + +`multi_stage` defers the expression until each referenced measure has been +aggregated on its own, so neither happens. + +### `dimensions` + +The `dimensions` parameter defines dimensions on the view itself, under the same +rule as [`measures`](#measures): the `sql` may only combine members the view +already includes. + + + +```yaml title="YAML" +views: + - name: orders_overview + cubes: + - join_path: orders.products + includes: + - category + - name + + dimensions: + - name: category_and_name + type: string + sql: "{CUBE.category} || ' / ' || {CUBE.name}" +``` + +```javascript title="JavaScript" +view(`orders_overview`, { + cubes: [ + { + join_path: orders.products, + includes: [`category`, `name`] + } + ], + + dimensions: { + category_and_name: { + type: `string`, + sql: `${CUBE.category} || ' / ' || ${CUBE.name}` + } + } +}) +``` + + + ### `folders` The `folders` parameter is used to organize members of a view (e.g., dimensions, @@ -991,6 +1110,8 @@ The `access_policy` parameter is used to configure [access policies][ref-ref-dap [ref-naming]: /docs/data-modeling/concepts/syntax#naming [ref-apis]: /reference [ref-ref-cubes]: /reference/data-modeling/cube +[ref-ref-multi-stage]: /reference/data-modeling/measures#multi_stage +[ref-multi-fact-views]: /docs/data-modeling/multi-fact-views [ref-ref-hierarchies]: /reference/data-modeling/hierarchies [ref-ref-dap]: /reference/data-modeling/data-access-policies [ref-rest-query-ops]: /reference/core-data-apis/rest-api/query-format#filters-operators diff --git a/packages/cubejs-schema-compiler/test/unit/multi-fact-derived-measure-in-view.test.ts b/packages/cubejs-schema-compiler/test/unit/multi-fact-derived-measure-in-view.test.ts new file mode 100644 index 0000000000000..aca4c631b85e1 --- /dev/null +++ b/packages/cubejs-schema-compiler/test/unit/multi-fact-derived-measure-in-view.test.ts @@ -0,0 +1,430 @@ +import { PostgresQuery } from '../../src/adapter/PostgresQuery'; +import { prepareYamlCompiler } from './PrepareCompiler'; + +// AOV ("basket") as a retailer models it: the numerator and the denominator sit +// in two different fact tables at two different grains. +// +// sales_line_item - one row per transaction line +// item_location_sales - one row per day/item/location +// +// `transactions_without_returns` is a count distinct of transaction ids on the +// line-item cube, narrowed by filters that belong to that cube (transaction +// type, fulfillment channel group). Those filters are written once, on the cube +// that owns the columns, and every consumer picks them up by including the +// measure - they are never restated in a view. `sales_amount` is a plain sum on +// the day/item/location cube. +// +// The ratio of the two is authored as a measure of the view rather than per +// consumer. The line-item side has to be aggregated to the query grain before +// it can divide a sum coming from the other fact table, which is the multi-fact +// path: both facts join to the shared `items`, `locations` and `dates` cubes, +// but never to each other. +const model = ` +cubes: + - name: items + sql: > + SELECT 1 AS id, 'Bakery' AS department UNION ALL + SELECT 2 AS id, 'Produce' AS department + dimensions: + - name: id + sql: "{CUBE}.id" + type: number + primary_key: true + - name: department + sql: "{CUBE}.department" + type: string + + - name: locations + sql: > + SELECT 1 AS id, 'West' AS region UNION ALL + SELECT 2 AS id, 'East' AS region + dimensions: + - name: id + sql: "{CUBE}.id" + type: number + primary_key: true + - name: region + sql: "{CUBE}.region" + type: string + + # Date spine shared by both facts. Without it the two facts have no common + # time member to stitch on: one is keyed by day, the other by timestamp. + - name: dates + sql: > + SELECT '2026-01-01'::timestamp AS date UNION ALL + SELECT '2026-01-02'::timestamp AS date + dimensions: + - name: date + sql: "{CUBE}.date" + type: time + primary_key: true + + - name: sales_line_item + sql: > + SELECT 1 AS id, 100 AS transaction_id, 1 AS item_id, 1 AS location_id, + 'SALE' AS transaction_type, 'IN_STORE' AS fulfillment_channel_group, + '2026-01-01'::timestamp AS sold_at + joins: + - name: items + sql: "{CUBE}.item_id = {items}.id" + relationship: many_to_one + - name: locations + sql: "{CUBE}.location_id = {locations}.id" + relationship: many_to_one + - name: dates + sql: "DATE_TRUNC('day', {CUBE}.sold_at) = {dates.date}" + relationship: many_to_one + dimensions: + - name: id + sql: "{CUBE}.id" + type: number + primary_key: true + - name: transaction_id + sql: "{CUBE}.transaction_id" + type: number + - name: transaction_type + sql: "{CUBE}.transaction_type" + type: string + - name: fulfillment_channel_group + sql: "{CUBE}.fulfillment_channel_group" + type: string + - name: sold_at + sql: "{CUBE}.sold_at" + type: time + segments: + - name: net_sale_transactions + sql: "{CUBE}.transaction_type NOT IN ('RETURN', 'EXCHANGE')" + measures: + - name: transactions_without_returns + sql: "{CUBE}.transaction_id" + type: count_distinct + filters: + - sql: "{CUBE}.transaction_type <> 'EXCHANGE'" + - sql: "{CUBE}.fulfillment_channel_group IN ('IN_STORE', 'SHIP_FROM_STORE')" + + - name: item_location_sales + sql: > + SELECT 1 AS id, 1 AS item_id, 1 AS location_id, + '2026-01-01'::timestamp AS date, 30 AS sales_amount + joins: + - name: items + sql: "{CUBE}.item_id = {items}.id" + relationship: many_to_one + - name: locations + sql: "{CUBE}.location_id = {locations}.id" + relationship: many_to_one + - name: dates + sql: "DATE_TRUNC('day', {CUBE}.date) = {dates.date}" + relationship: many_to_one + dimensions: + - name: id + sql: "{CUBE}.id" + type: number + primary_key: true + - name: date + sql: "{CUBE}.date" + type: time + measures: + - name: sales_amount + sql: "{CUBE}.sales_amount" + type: sum + +views: + - name: retail_analysis + cubes: + - join_path: item_location_sales + includes: + - sales_amount + - join_path: sales_line_item + includes: + - transactions_without_returns + - net_sale_transactions + # The shared dimension cubes sit at root-level join paths so their + # dimensions are common to both facts. + - join_path: dates + includes: + - date + - join_path: items + includes: + - department + - join_path: locations + includes: + - region + measures: + # References inside a view measure are resolved against the view, so both + # \`{CUBE.member}\` and \`{view_name.member}\` work - one of each below - while + # a bare \`{member}\` does not resolve at all. \`multi_stage\` is what lets the + # ratio be evaluated after both facts have been aggregated. + - name: aov_basket + type: number + multi_stage: true + sql: "{CUBE.sales_amount} / NULLIF({CUBE.transactions_without_returns}, 0)" + # Same expression without \`multi_stage\`, kept to pin what happens when + # the ratio is planned as an ordinary calculated measure. + - name: aov_basket_single_stage + type: number + sql: "{retail_analysis.sales_amount} / NULLIF({retail_analysis.transactions_without_returns}, 0)" +`; + +let compilers: any; + +beforeAll(async () => { + compilers = prepareYamlCompiler(model); + await compilers.compiler.compile(); +}); + +const buildSql = (query: any, useNativeSqlPlanner: boolean = true) => { + const [sql] = new PostgresQuery(compilers, { + timezone: 'UTC', + useNativeSqlPlanner, + ...query, + }).buildSqlAndParams(); + + return sql; +}; + +// Both facts, aggregated on their own before anything is combined. +const SALES_AMOUNT_AGGREGATE = /sum\("item_location_sales"\.sales_amount\)/; +const TRANSACTIONS_AGGREGATE = /COUNT\(DISTINCT CASE WHEN .* THEN "sales_line_item"\.transaction_id END\)/; +// The ratio, taken over the two per-fact aggregate columns once they are lined +// up on the query's dimensions. The subquery aliases the planner puts in front +// of those columns are deliberately not pinned - only that the numerator and +// denominator are the aggregated columns, in that order. +const RATIO_OVER_AGGREGATES = + /"item_location_sales__sales_amount" \/ NULLIF\("[^"]+"\."sales_line_item__transactions_without_returns", 0\)/; + +// Multi-fact queries are planned by Tesseract only, so everything that is +// expected to produce SQL runs against the native planner. +describe('Multi-fact derived measure defined on a view', () => { + it('aggregates each fact cube separately when the components are queried side by side', async () => { + const sql = buildSql({ + measures: [ + 'retail_analysis.sales_amount', + 'retail_analysis.transactions_without_returns', + ], + dimensions: ['retail_analysis.region'], + }); + + expect(sql).toMatch(SALES_AMOUNT_AGGREGATE); + expect(sql).toMatch(TRANSACTIONS_AGGREGATE); + // The line-item filters travel with the measure - the view does not restate + // them. + expect(sql).toContain('"sales_line_item".transaction_type <> \'EXCHANGE\''); + expect(sql).toContain('"sales_line_item".fulfillment_channel_group IN (\'IN_STORE\', \'SHIP_FROM_STORE\')'); + // Each fact reaches the shared dimension through its own join. + expect(sql).toContain('"item_location_sales".location_id = "locations".id'); + expect(sql).toContain('"sales_line_item".location_id = "locations".id'); + }); + + it('divides the two facts once both have been aggregated to the query grain', async () => { + const sql = buildSql({ + measures: ['retail_analysis.aov_basket'], + dimensions: ['retail_analysis.region'], + }); + + expect(sql).toMatch(SALES_AMOUNT_AGGREGATE); + expect(sql).toMatch(TRANSACTIONS_AGGREGATE); + expect(sql).toMatch(RATIO_OVER_AGGREGATES); + // The division is not pushed into either fact's own aggregation. + expect(sql).not.toMatch(/sum\("item_location_sales"\.sales_amount\) \/ NULLIF/); + }); + + it('divides the two facts on the shared date spine', async () => { + const sql = buildSql({ + measures: ['retail_analysis.aov_basket'], + timeDimensions: [{ dimension: 'retail_analysis.date', granularity: 'day' }], + }); + + expect(sql).toContain('DATE_TRUNC(\'day\', "item_location_sales".date) = "dates".date'); + expect(sql).toContain('DATE_TRUNC(\'day\', "sales_line_item".sold_at) = "dates".date'); + expect(sql).toMatch(RATIO_OVER_AGGREGATES); + }); + + it('returns the ratio next to its components', async () => { + const sql = buildSql({ + measures: [ + 'retail_analysis.sales_amount', + 'retail_analysis.transactions_without_returns', + 'retail_analysis.aov_basket', + ], + dimensions: ['retail_analysis.department'], + }); + + expect(sql).toMatch(SALES_AMOUNT_AGGREGATE); + expect(sql).toMatch(TRANSACTIONS_AGGREGATE); + expect(sql).toMatch(RATIO_OVER_AGGREGATES); + }); + + it('filters the ratio by a dimension shared between the facts', async () => { + const sql = buildSql({ + measures: ['retail_analysis.aov_basket'], + dimensions: ['retail_analysis.region'], + filters: [{ + member: 'retail_analysis.department', + operator: 'equals', + values: ['Bakery'], + }], + }); + + expect(sql).toMatch(RATIO_OVER_AGGREGATES); + // Both facts are narrowed, each through its own join to `items`. + expect(sql).toContain('"item_location_sales".item_id = "items".id'); + expect(sql).toContain('"sales_line_item".item_id = "items".id'); + }); + + // Current behaviour, pinned. Without `multi_stage` the ratio is planned as an + // ordinary calculated measure, so the planner looks for a single join tree + // covering both fact cubes and there is none - the two facts only meet + // through the shared dimensions. + it('cannot plan the ratio when the view measure is not multi_stage', () => { + expect(() => buildSql({ + measures: ['retail_analysis.aov_basket_single_stage'], + dimensions: ['retail_analysis.region'], + })).toThrow(/Can't find join path to join .*item_location_sales.*sales_line_item/); + }); + + // Current behaviour, pinned. A segment is the other place cube-owned filter + // logic could be written once and reused; it does not survive the multi-fact + // split, so shared filter logic has to live in the measure's own `filters:` + // (which does travel - see the first test). + it('cannot plan a multi-fact query that carries a segment', () => { + expect(() => buildSql({ + measures: ['retail_analysis.aov_basket'], + dimensions: ['retail_analysis.region'], + segments: ['retail_analysis.net_sale_transactions'], + })).toThrow(/Can't find join path to join/); + }); + + it('applies the segment when only its own fact is queried', () => { + const sql = buildSql({ + measures: ['retail_analysis.transactions_without_returns'], + dimensions: ['retail_analysis.region'], + segments: ['retail_analysis.net_sale_transactions'], + }); + + expect(sql).toContain('"sales_line_item".transaction_type NOT IN (\'RETURN\', \'EXCHANGE\')'); + }); + + it('is not planned by the legacy planner', () => { + expect(() => buildSql({ + measures: ['retail_analysis.aov_basket'], + dimensions: ['retail_analysis.region'], + }, false)).toThrow(/Can't find join path to join/); + }); + + // The reference forms a view measure accepts. `{CUBE.member}` and + // `{view_name.member}` are both in the model above; a bare `{member}` is + // rejected while the view is compiled, so it needs a model of its own. + it('rejects a bare member reference in a view measure', async () => { + const bareRef = model.replace( + '{CUBE.sales_amount} / NULLIF({CUBE.transactions_without_returns}, 0)', + '{sales_amount} / NULLIF({transactions_without_returns}, 0)' + ); + + await expect(prepareYamlCompiler(bareRef).compiler.compile()) + .rejects.toThrow(/sales_amount is not defined/); + }); +}); + +// The other reason a view measure spanning cubes wants `multi_stage`: even when +// the cubes DO join, a plain calculated measure is evaluated inside the single +// joined scan, so a `sum` on the one side is taken over rows the join has +// multiplied. `multi_stage` aggregates each side first, then divides. +describe('Derived view measure over a fanned-out join', () => { + const fanOutModel = ` +cubes: + - name: orders + sql: > + SELECT 1 AS id, 100 AS amount, 'NYC' AS city UNION ALL + SELECT 2 AS id, 200 AS amount, 'NYC' AS city + joins: + - name: line_items + sql: "{CUBE}.id = {line_items}.order_id" + relationship: one_to_many + dimensions: + - name: id + sql: "{CUBE}.id" + type: number + primary_key: true + - name: city + sql: "{CUBE}.city" + type: string + measures: + - name: total_amount + sql: "{CUBE}.amount" + type: sum + + - name: line_items + sql: > + SELECT 10 AS id, 1 AS order_id UNION ALL + SELECT 11 AS id, 1 AS order_id UNION ALL + SELECT 12 AS id, 2 AS order_id + dimensions: + - name: id + sql: "{CUBE}.id" + type: number + primary_key: true + measures: + - name: count + type: count + +views: + - name: orders_overview + cubes: + - join_path: orders + includes: + - total_amount + - city + - join_path: orders.line_items + includes: + - count + + measures: + - name: average_line_value + type: number + sql: "{CUBE.total_amount} / NULLIF({CUBE.count}, 0)" + - name: average_line_value_multi_stage + type: number + multi_stage: true + sql: "{CUBE.total_amount} / NULLIF({CUBE.count}, 0)" +`; + + let fanOutCompilers: any; + + beforeAll(async () => { + fanOutCompilers = prepareYamlCompiler(fanOutModel); + await fanOutCompilers.compiler.compile(); + }); + + const buildFanOutSql = (measure: string) => { + const [sql] = new PostgresQuery(fanOutCompilers, { + timezone: 'UTC', + useNativeSqlPlanner: true, + measures: [measure], + dimensions: ['orders_overview.city'], + }).buildSqlAndParams(); + + return sql; + }; + + // Current behaviour, pinned: `sum` runs over the multiplied rows of the join, + // so the numerator is larger than the same measure queried on its own. + it('inlines a plain calculated measure into the multiplied join', () => { + const sql = buildFanOutSql('orders_overview.average_line_value'); + + expect(sql).toMatch(/sum\("orders"\.amount\) \/ NULLIF\(count\("line_items"\.id\), 0\)/); + expect(sql).toContain('"orders".id = "line_items".order_id'); + }); + + it('aggregates each side before dividing when the measure is multi_stage', () => { + const sql = buildFanOutSql('orders_overview.average_line_value_multi_stage'); + + // `sum` is taken in a leg that never joins line_items, so nothing multiplies + // it. The span is tempered against `line_items` so the assertion fails if + // that leg ever picks the join back up. + expect(sql).toMatch(/sum\("orders"\.amount\) "orders__total_amount"(?:(?!line_items)[\s\S])*?GROUP BY 1/); + expect(sql).not.toMatch(/sum\("orders"\.amount\) \/ NULLIF/); + // The division happens over the two aggregated columns. + expect(sql).toMatch(/"orders__total_amount" \/ NULLIF\("[^"]+"\."line_items__count", 0\)/); + }); +}); From a3bc548ea1fffacccf6c77477cfb835b7d83429e Mon Sep 17 00:00:00 2001 From: Pavel Tiunov Date: Wed, 26 Aug 2026 14:53:02 -0700 Subject: [PATCH 2/5] fix(release): repair lerna-publish.sh's release step and bump cleanup (#11658) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(release): make lerna-publish.sh step 5 non-interactive and clean up staged files Step 5 ended with a dangling line continuation after --create-release=github, so the intended --yes never reached lerna. The release therefore stopped at lerna's "Are you sure you want to create these versions?" prompt and exited 1, which is fatal in any non-interactive/CI shell. The step 4 cleanup used `git restore .`, which only restores the working tree from the index. The cubestore `version` lifecycle hook (sync-cargo-version.js) stages rust/cubestore/Cargo.toml and Cargo.lock, so those stayed staged at the bumped version after any failure — leaving the tree dirty and making the next run abort on working tree validation. Restore the index as well. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015kRxi2jizmJQKt9cuv4vmg * fix(release): guard lerna-publish.sh cleanup against pre-existing work Addresses review feedback on the `git restore --staged --worktree .` cleanup. The restore discards staged as well as unstaged changes to tracked files, so on a dirty tree it would destroy work the operator had staged before invoking the script (an index-only change has no reflog to recover from). Gate the script on a clean tree up front, so cleanup can only ever undo what the script itself did. The gate checks tracked files only (--untracked-files=no). `git restore` cannot touch untracked paths, so they are outside the blast radius being guarded, and untracked build output that no .gitignore covers must not block a release. Also pair each cleanup with a tightly scoped `git clean`: lerna writes a CHANGELOG.md for any package that lacks one, and a newly created CHANGELOG.md is untracked, so `git restore` leaves it behind to trip lerna's working tree validation on the next run. Scoping the clean to '*CHANGELOG.md' removes exactly those files and leaves every other untracked path alone. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015kRxi2jizmJQKt9cuv4vmg * fix(release): clean up lerna-publish.sh bump from an EXIT trap Addresses further review feedback. `set -e` aborts the script before the inline step 4 cleanup if step 1's lerna call or step 2's `yarn install` fails, leaving exactly the bumped, partly staged tree these fixes exist to avoid — and a `yarn install` failure is a realistic path, not a theoretical one. Move cleanup into an EXIT trap so it also covers those abort paths, and drop the two duplicated inline blocks. The trap is installed after the pre-flight gate, never before, so a tree the gate rejects is left untouched; step 4 clears the trap because the version commit, tag and release are meant to survive. The handler preserves the original exit status. The gate's advice now also names the discard command, since leftover state from a failed run is the likeliest way to trip it and neither committing nor stashing a half-finished version bump is what the operator wants. Scope the clean pathspecs to 'packages/*/CHANGELOG.md' and 'rust/*/CHANGELOG.md', matching the workspace globs in package.json, so an untracked CHANGELOG.md the operator happens to hold elsewhere in the tree is no longer removed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015kRxi2jizmJQKt9cuv4vmg * fix(release): cover an interrupted release run in the cleanup trap Review nit. The window the trap protects is two long-running commands, so an operator interrupting the run is a likely way out of it. bash does run an EXIT trap for an untrapped SIGINT (verified on 5.2.21, both signalling the process directly and the process group), but naming INT and TERM costs nothing and states the guarantee instead of resting it on that behaviour. The handler disarms itself before doing anything, so its own `exit` cannot run the cleanup a second time now that a signal reaches it directly. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015kRxi2jizmJQKt9cuv4vmg * fix(release): make an interrupted release run exit non-zero Naming INT and TERM in the trap left the handler exiting with `$?`, which on a signal path is the last *completed* command's status - 0 when the signal lands between commands. An interrupted run therefore reported success: verified exit code 0 after a SIGINT mid-bump, which would let `./lerna-publish.sh && ...` carry on as though the release had happened. Re-raise the signal instead, once the handler has disarmed itself and cleaned up, so the caller sees the run was interrupted. The `exit $status` path is kept for the EXIT trap, where `$?` is the real failure code. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015kRxi2jizmJQKt9cuv4vmg * fix(release): confirm the release step by default, --yes to skip The release step commits, tags, pushes and creates the GitHub release, so passing --yes unconditionally took away the operator's last look before all of that goes out. Make the prompt the default again and let the caller opt out. ./lerna-publish.sh # asks before releasing ./lerna-publish.sh --yes # unattended ./lerna-publish.sh minor --yes # bump still positional Step 1's bump keeps --yes unconditionally: the cleanup trap throws that bump away, so there is nothing there for an operator to confirm. lerna's prompt cannot be answered where stdin is not a terminal - it fails with a bare exit 1 that says nothing about why, which is how this surfaced in the first place. Check for that up front, before the bump rather than after it, and say what to pass instead. Also quote "$BUMP" at both call sites, now that it comes out of an argument loop rather than straight from $1. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015kRxi2jizmJQKt9cuv4vmg * fix(release): restore the happy-path cleanup the trap refactor dropped The two `git restore .` sites were not duplicates of each other. The one before the release step is a required step of the happy path: step 1's bump has to come back out before the real `lerna version` runs, because lerna reads the versions in the tree to compute the next ones and refuses to commit over a dirty tree at all. Folding both sites into the exit handler and then disarming it before the release step meant that on a successful run the bump was never undone, so the release ran on a still-bumped tree - it would abort on checkWorkingTree, or bump a second time on top of step 1 if it got past that. Split the restore into restore_bump, call it explicitly where master did, and have the trap call the same function for the runs that never reach it. Restore first, then disarm, so an interrupt during the restore is still covered. The step numbering goes back to master's 1-5 now that the cleanup step is a step again. Reproduced with a stub that reports `git status --porcelain` at the start of the release step: before this it read "M CONTRIBUTING.md M README.md", now empty. The earlier stub matrix could not see it - a stub yarn neither validates the working tree nor recomputes versions, so the "clean tree, full run" row recorded the missing cleanup as expected behaviour. That row now asserts the tree. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015kRxi2jizmJQKt9cuv4vmg * fix(release): let --help work without a .gh-token `. .gh-token` ran before the argument loop, so under `set -e` the usage output I added in the previous commit died on any checkout without the token file - which is every fresh clone, since .gh-token is gitignored: $ ./lerna-publish.sh --help ./lerna-publish.sh: line 4: .gh-token: No such file or directory Source it after the argument and terminal checks instead. Nothing before that point needs GH_TOKEN; only the release step's --create-release=github does, and a run that gets that far still fails the same way if the file is missing. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015kRxi2jizmJQKt9cuv4vmg --------- Co-authored-by: Claude --- lerna-publish.sh | 106 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 97 insertions(+), 9 deletions(-) diff --git a/lerna-publish.sh b/lerna-publish.sh index f610139cbf630..29a9181c7e977 100755 --- a/lerna-publish.sh +++ b/lerna-publish.sh @@ -1,15 +1,97 @@ #!/bin/bash set -e +usage() { + echo "Usage: $0 [bump] [-y|--yes]" + echo + echo " bump version bump to pass to lerna (default: patch)" + echo " -y, --yes skip lerna's confirmation prompt for the release step" + echo + echo "The release step asks for confirmation before it commits, tags, pushes and" + echo "creates the GitHub release. Pass --yes to run it unattended." +} + +BUMP=patch +CONFIRM=() + +while [ $# -gt 0 ]; do + case "$1" in + -y|--yes) CONFIRM=(--yes) ;; + -h|--help) usage; exit 0 ;; + -*) echo "Error: unknown option: $1"; echo; usage; exit 1 ;; + *) BUMP="$1" ;; + esac + shift +done + +# Without --yes the release step prompts, and lerna's prompt cannot be answered +# where stdin is not a terminal - it fails there with a bare exit 1 that says +# nothing about why. Say it here instead. +if [ ${#CONFIRM[@]} -eq 0 ] && [ ! -t 0 ]; then + echo "Error: stdin is not a terminal, so the release step's confirmation prompt" + echo "cannot be answered. Re-run with --yes to release unattended." + exit 1 +fi + +# GH_TOKEN, for the release step's --create-release=github. Sourced after the +# argument checks so that --help works on a checkout that has no .gh-token yet +# - the file is gitignored, so a fresh clone never has one. . .gh-token -BUMP=$1 -if [ "x$BUMP" == "x" ]; then - BUMP=patch +# Cleanup below discards staged as well as unstaged changes to tracked files. +# Refuse to start if the tree already carries any, so that cleanup can only ever +# undo what this script itself did. This gate runs before the trap is installed, +# so a tree it rejects is never touched. +# Untracked files are deliberately not checked: `git restore` cannot touch them, +# and build output that no .gitignore covers must not block a release. +if [ -n "$(git status --porcelain --untracked-files=no)" ]; then + echo "Error: working tree has uncommitted changes to tracked files." + echo "Commit or stash them before releasing - cleanup would discard them." + echo "If this is leftover state from a failed release run, discard it with:" + echo " git restore --staged --worktree ." + GIT_PAGER=cat git status --short --untracked-files=no + exit 1 fi +# Step 4 undoes the bump on the happy path, but `set -e` aborts before reaching +# it if step 1 or step 2 fails, so the same restore also runs from a trap: +# otherwise a failed run leaves behind the bumped, partly staged tree that trips +# lerna's working tree validation on the next run. +# lerna also writes a CHANGELOG.md for any package that lacks one, and a newly +# created one is untracked, so `git restore` alone would leave it behind - the +# pathspecs match the workspace globs ("workspaces" in package.json) and +# nothing else in the tree. +# INT and TERM are named alongside EXIT so an interrupted run is covered without +# relying on the shell running an EXIT trap for an untrapped signal; the handler +# disarms itself first so its own `exit` cannot run the cleanup a second time, +# then re-raises the signal so an interrupted run does not exit 0. +restore_bump() { + git restore --staged --worktree . || true + git clean -fdq -- 'packages/*/CHANGELOG.md' 'rust/*/CHANGELOG.md' || true +} + +cleanup_bump() { + status=$? + sig=$1 + trap - EXIT INT TERM + echo "Cleaning up temporary version bump..." + restore_bump + if [ -n "$sig" ]; then + # Die from the signal now that it is untrapped, so the caller sees the run + # was interrupted. Exiting with $? instead would report the last completed + # command's status, which is 0 when the signal lands between commands. + kill -"$sig" $$ + fi + exit $status +} +trap cleanup_bump EXIT +trap 'cleanup_bump INT' INT +trap 'cleanup_bump TERM' TERM + +# Always unattended: this bump is thrown away by the cleanup above, so there is +# nothing for an operator to confirm. Only the release step below asks. echo "Step 1: bumping versions (no commit/push)..." -yarn lerna version $BUMP \ +yarn lerna version "$BUMP" \ --conventional-commits \ --force-publish \ --exact \ @@ -26,18 +108,24 @@ if git status --porcelain | grep -q '^ M yarn.lock'; then echo "If you see any new entries in yarn.lock with @cubejs-*/* packages - probably not all packages versions were updated." GIT_PAGER=cat git diff yarn.lock - echo "Step 4: cleaning up temporary version bump..." - git restore . - exit 1 fi +# Step 1's bump has to come back out before the real one: lerna reads the +# versions in the tree to compute the next ones, and refuses to commit over a +# dirty tree at all. This is the happy path, not error handling - the trap +# below only covers the runs that never get here. echo "Step 4: cleaning up temporary version bump..." -git restore . +restore_bump + +# Restore first, then disarm, so an interrupt during the restore is still +# covered. From here the version commit, tag and release are meant to survive. +trap - EXIT INT TERM echo "Step 5: commit, tag and push version..." -yarn lerna version $BUMP \ +yarn lerna version "$BUMP" \ --conventional-commits \ --force-publish \ --exact \ --create-release=github \ + "${CONFIRM[@]}" From 74da9f692a9b2fc102d3cbd501b16858c33c780f Mon Sep 17 00:00:00 2001 From: Pavel Tiunov Date: Wed, 26 Aug 2026 15:16:07 -0700 Subject: [PATCH 3/5] docs(ai): describe agent-config reconciliation instead of UI-first creation (#11657) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(ai): describe agent-config reconciliation instead of UI-first creation The multi-agent page claimed spaces and agents must first be created through the UI before they can be configured via YAML. It is the other way around: agents/config.yml is the source of truth, agents cannot be created in the UI and their settings are read-only there, and Cube creates the records from the config when a deployment is reconciled. Replace that callout and document the reconciliation flow that actually ships — Pending Configurations on the Agents and Spaces pages, the Reconcile agent configs button in the Semantic Model IDE, Create All, and what reconciliation does not cover (property, rule and certified-query edits apply straight from the config). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N6JE5RQgcZZ3rXMcgH7Y6Q * docs(ai): qualify what reconciliation covers, and who can be created by hand Review follow-ups on the reconciliation section, each checked against the Cloud code: - An agent's `space` link is made at creation. Changing it in YAML does not move an existing agent — the agent page flags the mismatch, and the relink happens on the next reconcile. The blanket "every other property takes effect without reconciling" claimed otherwise. - `description` is read from the config at request time, so it stays in the list of properties that need no reconcile. - Spaces can be created by hand, unlike agents, but such a space carries no link to a `spaces:` entry, so the entry still reads as pending. - Name where a leftover record is deleted: the Agents or Spaces page, which both offer a delete row action. Prose is unwrapped to one line per paragraph to match the rest of the page. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N6JE5RQgcZZ3rXMcgH7Y6Q * docs(ai): say how to act on an agent whose space no longer matches its config The space-mismatch paragraph said reconciling again moves the agent without saying what to press, and the answer depends on the case: - the newly named space has no record yet — it lists under Pending Configurations, and Create All creates it and relinks the agent in one action. - the space already exists for the deployment — nothing is pending, the panel renders nothing, so there is no Create All at all. Deleting the agent makes its config pending again and the next Create All recreates it in the space the config names. Both paths verified against the reconcile action and the agent settings page, where the space row is a read-only link and the config assignment is locked. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N6JE5RQgcZZ3rXMcgH7Y6Q * docs(ai): move the by-hand space caveat to the section that documents it The caveat was in the opening callout, which is where it was least likely to be read: a reader who lands on Space scope sees the "Create space" path without it. Moving it onto that bullet also relieves the densest paragraph on the page, and says what actually goes wrong — the hand-made space has no link to a spaces: entry, so the entry stays pending and reconciling creates a second space. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N6JE5RQgcZZ3rXMcgH7Y6Q * docs(ai): state the invariant for a space mismatch, and what a delete costs "There is no Create All to press" only held when nothing else was pending — the panel renders whenever any entry is, so a reader who added a second agent in the same edit sees the button and would go looking for a UI bug. The invariant is that Create All won't move the mismatched agent; the button's absence is the special case. The delete-and-recreate remedy also needed its cost stated. Traced rather than assumed: chat threads live in ai-engineer's own database with a plain agentId column, no foreign key or cascade onto the console-server agents table, and deleting an agent is a bare row destroy — so nothing is dropped from storage, but the recreated agent is a new record and earlier chats don't carry over to it. The page says that, and not that deleting discards them. Also fold the config file into Step 1's body, and reword the rename warning so the old record "stays behind with everything tied to it" rather than "keeps what it stored", which described storage it doesn't own. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N6JE5RQgcZZ3rXMcgH7Y6Q * docs(ai): matching is on the config link, not on any record's name Moving the by-hand-space caveat into Space scope exposed a contradiction the page had been carrying: it promised entries are matched "to what already exists" by name, while the caveat says a hand-made space with the identical name does not satisfy the entry. The caveat is what the code does. Every record reconciliation creates stores the config entry it came from, and matching compares that stored value — a space created by hand has none, so the entry stays pending and reconciling creates a second space. Both places that made the broader claim now scope it to the records Cube created from the config. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N6JE5RQgcZZ3rXMcgH7Y6Q * docs(ai): finish the sweep — a per-deployment space is linked to its entry The previous commit rewrote the two places that described matching as name-against-existing-records and missed a third, pre-existing one: the per-deployment naming paragraph said the space stays linked to the name declared in agents/config.yml. Its conclusion was right but the mechanism was the one the Create-space bullet three lines above depends on being different — a hand-made space with an identical name doesn't match because it has no entry link. Swept the rest of the page: the remaining "linked"/"matching" sentences describe which space an agent resolves to, or how scope is inferred from agents, and are unaffected. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N6JE5RQgcZZ3rXMcgH7Y6Q * docs(ai): a global space counts as created for every deployment Step 2 said reconciliation compares the config with the records it created "for that deployment", which is wrong for a global space: it is account-level, so an entry reads as reconciled on every deployment even though the space was created while reconciling one of them. That is what the pending check actually asks — whether a record is available to the deployment, not whether it was created for it — and the Space scope warning 30 lines below is too late for a reader forming the model from Step 2. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N6JE5RQgcZZ3rXMcgH7Y6Q * docs(ai): a per-deployment entry is pending on every deployment separately "You need reconciliation only when a new name appears in the config" was a config-global rule summarizing a per-deployment check, and the page already documented the case where the two diverge: the same spaces: entry declared in several deployments, scoped per deployment. Reconciling on production creates a record available only to production, so staging still lists that entry as pending with no new name anywhere in the config — which reads as a stale list to anyone taking the rule literally. Also carry Step 2's vocabulary into the mismatch paragraph: a space is "available to" a deployment rather than "exists for" it, which is the accurate phrasing for a global space. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N6JE5RQgcZZ3rXMcgH7Y6Q * docs(ai): scope belongs to the record, and agents recur per deployment Two errors in the rule sentence, both of the kind this PR exists to remove. "A per-deployment entry" attributes scope to the config entry, but nothing in spaces: declares it — scope is chosen in the Create All panel or on the space page, so a reader would go looking in the spaces: reference for a key that doesn't exist. The examples also omitted agents, which are the commonest case: an agent record carries a deploymentId, so on a deployment reconciled for the first time every agents: entry is pending with no new name and no space scoping involved. Restated as what recurs per deployment — an agent, and a space created per deployment — against the global space that counts as created everywhere. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N6JE5RQgcZZ3rXMcgH7Y6Q * docs(ai): spaces are not hand-creatable either, so stop documenting that path The page presented creating a space by hand as one of two places you pick the scope, with a caveat about the entry staying pending. Manual creation is not a supported path for either kind of record: both come from the config, and the UI creates them when you reconcile. Scope selection now has one documented home — the Create All panel while reconciling, plus re-scoping an existing space on its page. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N6JE5RQgcZZ3rXMcgH7Y6Q * docs(ai): state the supported path without over-claiming what the UI blocks Two problems with the sentence that replaced the by-hand path. "Their settings are read-only there" was asserted of spaces as well as agents, and the Space scope section three sections down tells you to change a space's scope on its page — which the split-a-shared-space instruction depends on. Read-only was only ever verified for agents. "Neither spaces nor agents can be created in the UI" is also stronger than what a reader can see: the Create space control exists, it just produces a space with no link to a spaces: entry. Asserting it away invites the same "the docs are wrong" reaction as the callout this PR removed. Both replaced by the claim that actually holds: declaring in the config and reconciling is the only supported way to create either. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N6JE5RQgcZZ3rXMcgH7Y6Q --------- Co-authored-by: Claude Opus 5 --- docs-mintlify/admin/ai/multi-agent.mdx | 37 ++++++++++++++++++++------ 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/docs-mintlify/admin/ai/multi-agent.mdx b/docs-mintlify/admin/ai/multi-agent.mdx index b919d458947d2..5aa04cb3adfb7 100644 --- a/docs-mintlify/admin/ai/multi-agent.mdx +++ b/docs-mintlify/admin/ai/multi-agent.mdx @@ -11,9 +11,9 @@ Multi-agent is useful when: - You want specialized agents with distinct instructions or tool access in the same deployment. - You need to isolate context (rules, certified queries, memories) between user groups. - - Spaces and agents must first be created through the Cube Cloud UI before they can be configured via YAML. The system matches YAML entries to UI-created spaces and agents by their `name` field. - + + `agents/config.yml` is the source of truth for spaces and agents: declaring them there and [reconciling](#reconciliation) is the only supported way to create either. Agents can't be created in the UI, and their settings are read-only there. Cube links every record it creates to the entry it came from, and matches them by that entry's `name`. + ## Architecture @@ -128,6 +128,30 @@ spaces: Each agent must reference exactly one space via its `space` property. Multiple agents can share the same space and inherit its rules, certified queries, and memories. +## Reconciliation + +A space or agent declared in `agents/config.yml` needs a matching record in Cube before users can chat with it. Cube creates those records from your config — that step is **reconciliation**: + + + + Add the `spaces:` and `agents:` entries to `agents/config.yml`. In dev mode the pending list reflects your branch, so you can reconcile before merging. + + + Cube compares the config with the records already available to that deployment, matching each entry by `name`. Entries with no record yet are listed under **Pending Configurations** on the **Agents** page and on **Agents** → **Spaces**, once you pick the deployment. The Semantic Model IDE also shows a **Reconcile agent configs** button with the pending count that links there. + + + Choose the [space scope](#space-scope) and press **Create All**. Spaces are created first, then each agent is linked to the space its `space` property names. + + + +Reconciliation only creates missing spaces and agents, so you need it only when an entry has no record available to the deployment yet: after adding a `name` to the config, and on each deployment you reconcile for the first time — an agent belongs to one deployment, as does a space created per deployment, while a global space counts as created everywhere. Agent behavior — `llm`, `description`, `accessible_views`, `memory_mode`, rules, certified queries — is read from the `agents/` directory of the deployment's data model and takes effect without reconciling. The implicit `auto` space and agent of the [single-agent setup](/admin/ai) are never listed as pending. + +An agent's `space` is the exception: the link is made when the agent is created, so changing it in YAML doesn't move an existing agent. The agent's page flags the mismatch between the space its config names and the space it is linked to. If the newly named space has no record yet, it appears under **Pending Configurations**, and **Create All** creates it and moves the agent onto it in the same action. If that space is already available to the deployment, nothing about the agent is pending and **Create All** won't move it — delete the agent so its config reads as pending again, then **Create All** recreates it in the space the config names. The recreated agent is a new agent, so chats from before the delete don't carry over to it. + + + Changing an entry's `name` reads as a new entry: reconciling creates a new space or agent, and the record created from the old name stays behind with everything tied to it — an agent's chats, a space's memories — flagged **(misconfigured)** because its config no longer exists. Delete it from the **Agents** or **Spaces** page once you no longer need it. + + ## Space scope Spaces live at the account level, so one space can be used by agents in more than one deployment. When a space is created, you choose its scope: @@ -135,12 +159,9 @@ Spaces live at the account level, so one space can be used by agents in more tha - **Global** — one space that agents in every deployment can use. - **Per deployment** — the space is available to a single deployment only. -You pick the scope on the **Agents** → **Spaces** page, in two places: - -- **Create space**, when you create a space by hand. -- The **Create All** panel, shown for spaces and agents declared in `agents/config.yml` that don't exist yet. Its **Create Spaces** control offers **Global** and **Per deployment**. +You pick the scope when you [reconcile](#reconciliation): the **Create All** panel's **Create Spaces** control offers **Global** and **Per deployment**. You can change the scope of an existing space later on its page, under **Agents** → **Spaces**. -Spaces created per deployment are named after the deployment, for example `Product (production)` and `Product (staging)`. Only the displayed name changes — the space stays linked to the space `name` declared in `agents/config.yml`, so the YAML entry keeps matching. You can change the scope of an existing space later on the space's page. +Spaces created per deployment are named after the deployment, for example `Product (production)` and `Product (staging)`. Only the displayed name changes — the space stays linked to the `spaces:` entry it was created from, so the YAML entry keeps matching. Choose per-deployment scope when the same `spaces:` entry is declared in several deployments — typically development, staging, and production fed from branches of one data model — and you don't want them sharing what the space stores. From d9ff741b991ae9fb0bde5b742f6d514f3a7f94d7 Mon Sep 17 00:00:00 2001 From: Pavel Tiunov Date: Wed, 26 Aug 2026 15:23:19 -0700 Subject: [PATCH 4/5] fix(cubestore-driver): don't fail queries on `write EPIPE`, report over-limit messages readably (#11490) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(cubestore-driver): resend queries when the write to Cube Store fails When Cube Store closes a connection (restart, rolling deploy, idle drop), the driver can still see the socket as OPEN and write into it. That write fails with `write EPIPE`, and the query was rejected right away with `ConnectionError: CubeStore connection error: write EPIPE`, surfacing as an internal error in the SQL API and the REST API. The connection already knows how to recover: the 'close' handler resends everything that is still pending over a freshly established connection. The failed write raced with it and rejected the query before the resend could deliver it, even though the query had never reached Cube Store. Keep such a message registered in `sentMessages` and terminate the broken socket instead of rejecting, so the existing resend path delivers it. The same applies to writes that fail while resending. Resends are bounded by `CUBEJS_CUBESTORE_MAX_CONNECT_RETRIES` so a message can't retry forever, and a write error on an already closed socket that no resend will pick up still rejects as before. Adds an e2e test suite that runs the driver against a Cube Store mock over real sockets and breaks the connection in the ways that produce this error. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GPf8PuEMdZ6RE7BusNGWej * fix(cubestore-driver): report a readable error when a message is over the size limit A query result bigger than the connection accepts (`ws` maxPayload, 100 MB) tore the connection down without an explanation: the query was then resent over a new connection, produced the same oversized response, and repeated until the retry budget ran out, ending in `CubeStore connection lost: message wasn't delivered after N retries` after N re-executions of an expensive query. Concurrent queries on the same connection failed with `write EPIPE`. Report the size limit instead, and don't retry what can't succeed: MessageTooLargeError: Cube Store response size exceeds the maximum message size of 100 MB. Reduce the amount of data the query returns, e.g. by adding filters or a limit, or raise CUBEJS_CUBESTORE_MAX_MESSAGE_SIZE. The limit is now explicit and configurable through CUBEJS_CUBESTORE_MAX_MESSAGE_SIZE (default 100 MB, same as the `ws` default it replaces), and it applies to outgoing messages too: a query larger than the limit is rejected before it is sent, since Cube Store would close the connection on it and that surfaces as an unrelated `write EPIPE`. A peer closing the connection with 1009 is reported the same way. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GPf8PuEMdZ6RE7BusNGWej * fix(cubestore-driver): address review on message size handling - Don't fail unrelated queries with a size error. The connection multiplexes messages and `ws` drops an oversized frame before its message id is read, so a message that was alone in flight is the only one that can be attributed. Otherwise every message gets one more round, which answers the innocent ones and leaves the offender alone on the connection, where the next round does attribute it. Whatever is still in flight after that round is failed regardless, so an offender that keeps killing the connection before the others are answered can't turn into a re-send loop. - Don't register a message on a socket whose 'close' already fired: nothing would write it and the re-send loop of that socket has already taken its snapshot, so the query would never settle. `openSocket()` now establishes a fresh connection instead, and rejects rather than hanging if that fails. - Note that the client and Cube Store message size limits are independent: the outgoing check only catches what is over the client's own limit, while a query over Cube Store's stricter CUBESTORE_TRANSPORT_MAX_MESSAGE_SIZE is reported when Cube Store closes the connection. - Format sizes below 1 MB in KB or bytes, so a small configured limit doesn't read as "0 MB". Tests: a small query in flight alongside an over-limit response is answered rather than failed, a successful result round trip is asserted (with only the native result decoder stubbed), and the buffered-write setup waits for the frame to reach the write buffer instead of a single loop turn. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GPf8PuEMdZ6RE7BusNGWej * fix(cubestore-driver): register the whole re-sent batch before writing it Writing yields, so a socket that closed in the middle of a re-send batch saw only the messages registered so far. When that set was a single message, the fatal branch rejected it and returned without scheduling another re-send, and the messages registered after that landed on a socket nobody would write from or re-send again: they never settled. Register the batch in one synchronous pass, then write it, so 'close' always sees all of it. A message answered or failed while the batch is being written is no longer written. The test observes the socket at the first re-send write, which is the only point where the two orders differ: by the time the mock server receives that message the driver has long finished the batch either way. Also make the concurrent-offender test order the two responses explicitly instead of spacing them 50 ms apart, so it doesn't depend on the driver being scheduled promptly under CI load. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GPf8PuEMdZ6RE7BusNGWej * refactor(cubestore-driver): rely on the existing resend loop The fix had grown a second retry path next to the one the 'close' handler already implements: a per-message resend budget, a per-message counter of how many connections died under it, an `openSocket()` that re-established a connection before registering, and a two-pass re-send that registered a batch before writing it. None of that is needed to fix the reported failures. A failed write now just terminates the socket, which is exactly the event the existing re-send loop already handles, and the loop itself goes back to what it was. Bounding the re-sends is left to the connection-level retry, as before; the only case that could genuinely spin -- an over-limit message that no re-send can fix -- is failed rather than re-sent. Attributing an over-limit response is now a single condition: `ws` drops the frame before its message id is read, so the error is reported when the message was alone in flight, and everything else takes another round, which answers the innocent queries and leaves the offender alone to be named next time. Net effect on the change: -182 lines in WebSocketConnection.ts, one test dropped with the machinery it covered. The remaining 10 tests pass and still fail against the unfixed source. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GPf8PuEMdZ6RE7BusNGWej * fix(cubestore-driver): bound the re-sends of an unattributable failure Review of the simplification caught that `pending.length === 1` as the only attribution rule doesn't terminate. An over-limit response that arrives while a second query is in flight is never attributed: both messages are re-sent, Cube Store hands the offender back its cached result ahead of the other answer, and the pending set is unchanged when the connection dies again. Nothing bounds that -- `currentConnectionTry` isn't touched on the over-limit path and every pong resets it -- so it's a 1s loop of re-execution and teardown with neither promise settling. The new test reproduces it: an oversized response that always beats the small query's answer hung to the 60s jest timeout, and now settles in about a second. One counter per message restores the bound: an unattributable failure gives every message in flight one more round, which answers the innocent ones and usually leaves the offender alone to be named next time, and whatever is still in flight after that round is failed regardless. Also from review, both reachable through the fatal branch added here: - The re-send loop registered messages on the new socket interleaved with awaited writes, so a socket dying mid-batch could strand the rest of it with nobody left to write or re-send them. The batch is now registered in one pass before any of it is written. - A message registered on a socket whose 'close' had already fired now re-enters `sendMessage`, which establishes a fresh connection, rather than failing a query that was never written. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GPf8PuEMdZ6RE7BusNGWej * fix(cubestore-driver): count only consecutive unattributable failures `fatalRounds` was cumulative over a message's whole lifetime, so the extra round it grants could be spent long before it was needed: a query that was merely in flight during one over-limit incident, then survived ordinary disconnects for minutes, was rejected with MessageTooLargeError in the first round of a later incident it had nothing to do with -- the misattribution the counter exists to prevent. Resetting it on a close that isn't fatal keeps the bound, since the loop it bounds is fatal every round, and makes the counter mean what its comment says. The new test walks a size incident, an ordinary disconnect and a second size incident with the same slow query in flight throughout; without the reset that query fails with the size error instead of being answered. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GPf8PuEMdZ6RE7BusNGWej * fix(cubestore): answer an over-limit message with close code 1009 An incoming message over CUBESTORE_TRANSPORT_MAX_MESSAGE_SIZE or CUBESTORE_TRANSPORT_MAX_FRAME_SIZE surfaced as a capacity error from the WebSocket stream, which the handler's catch-all logged before breaking out of the loop. That dropped the connection without a close frame, so the client saw a bare disconnect it could only read as Cube Store going away, and retried the query that caused it. The error is raised as soon as the frame header is parsed and before the payload is read, so the frame stream is left desynchronized and the message id is never seen: the connection can't be reused, and an application level error can't be attributed to the query that caused it. The capacity error is now recognized and answered with 1009 ("Message Too Big"), the close code the WebSocket protocol reserves for exactly this, carrying the size and the limit as the reason, so the client can report the size instead of retrying a disconnect. Every other break in the loop is unchanged. The test drives the server with a 4 KB limit over a real socket and asserts the close frame; against the previous code it fails with the reported `Connection reset by peer` instead. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GPf8PuEMdZ6RE7BusNGWej * test(cubestore): bind the message size test to its own port master's new ws_process_id_header_test binds 53032, which this test was using too. Tests in the crate share a binary and run in parallel, so the two would race for the address once the branches meet. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GPf8PuEMdZ6RE7BusNGWej * fix(cubestore): answer an over-limit request with an error, not a close The size limit was enforced by the transport, which refuses the frame as soon as its header is parsed and before the payload is read. That leaves the frame stream desynchronized and the message id unread, so the only possible answer was a close frame — taking down every other query multiplexed over that connection along with the oversized one. Enforce the limit a layer up instead, once the message has arrived and its id is known: the client gets an error naming the request that was too big, and the connection stays up for everything else in flight on it. The transport keeps the limit as a backstop, at a factor above the configured value, so a peer still can't make the server buffer without bound; past that backstop the close frame remains the answer. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GPf8PuEMdZ6RE7BusNGWej * docs(cubestore): explain why the frame limit gets the size headroom too Review asked why TRANSPORT_SIZE_HEADROOM is applied to the frame limit when only the message size is re-checked in the handler. It has to be: a query arrives as a single frame, so an exact frame limit refuses an over-limit message at the transport before the handler sees it, and at the default configuration — both knobs 64 << 20 — that is every over-limit message, which would leave the readable error unreachable. Frame size cannot be re-checked up here, since reassembly happens below warp. Say so, along with what it means when the two knobs are configured apart: the frame knob bounds a single allocation at the headroom multiple of its value, and the message limit enforces the policy. Also add the blank line between the two size tests and the full stop the client-side error text already has. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GPf8PuEMdZ6RE7BusNGWej * fix(cubestore-driver): give every message its extra round after a fatal close A message that was alone in flight was failed on the spot, on the reasoning that an unattributable oversized response must belong to the only query there was. But a fatal close on a sole in-flight query is not proof the query is at fault -- the same close is what an ordinary disconnect looks like -- and re-sending is what recovers that case. Drop the special case, so every message in flight gets the one extra round the counter already grants: the innocent ones are answered, and a genuine offender spends its round and is reported with its size on the next fatal close. The bound is unchanged, since it was never the one doing the bounding. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GPf8PuEMdZ6RE7BusNGWej * docs: an over-limit Cube Store message is answered, not dropped #11655 documented the transport size limits while this change was in flight, so it describes the behaviour this branch removes: a message above the transport limit closing the connection, surfacing as `write EPIPE`, recorded as a known limitation. It is answered with an error naming the size now, and the connection survives for the other queries multiplexed over it. Also document CUBEJS_CUBESTORE_MAX_MESSAGE_SIZE, which this change adds, next to the Cube Store side limits it is independent of. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GPf8PuEMdZ6RE7BusNGWej * fix(cubestore): report the configured size limit, not the backstop The close reason and the log line formatted tungstenite's `max_size`, which is what the transport was configured with -- the configured limit times TRANSPORT_SIZE_HEADROOM. So they named a number no operator set, double the real one, while a message between the two is refused by the handler check against the un-doubled value. Divide the headroom back out rather than formatting the message limit directly, so that a CUBESTORE_TRANSPORT_MAX_FRAME_SIZE configured below the message limit reports itself when it is the one that fired. Both size tests now assert the configured number instead of just the prefix, which is the assertion that would have caught this: against the previous code the close reason reads 8192 where the limit is 4096. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GPf8PuEMdZ6RE7BusNGWej * docs: correct why a sole in-flight message gets a round, and the backstop Two things review caught, both about accuracy of the explanation rather than behaviour. The comment justified the extra round for a sole in-flight message with "the close can just as well be an ordinary disconnect". That is not true inside this branch: `fatalError` is only set when `ws` saw an oversized frame or the peer closed with 1009, and an ordinary disconnect leaves it null and takes the reset path instead. The real ambiguity is that the frame says an oversized message arrived, not which query produced it -- it can belong to a query already rejected on an earlier round whose response was still on the wire. The architecture page promised the connection stays up without saying where that stops. Past twice the configured limit the message is refused before it can be attributed and the connection is closed after all, which is the case an operator reading the page would be looking up. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GPf8PuEMdZ6RE7BusNGWej * fix(cubestore-driver): surface the sizes Cube Store sends with a 1009 close The 'close' handler bound only the code and dropped the reason, so the concrete numbers Cube Store puts there never reached the user: they were told to raise CUBESTORE_TRANSPORT_MAX_MESSAGE_SIZE without being told what it is or how far over they went. On the server side that only exists in the Cube Store log, which is where an API user cannot look. Append the reason when there is one. A peer that closes with 1009 and no reason -- an intermediary, or an older Cube Store -- keeps the generic wording, which is now covered by its own test. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GPf8PuEMdZ6RE7BusNGWej * fix(cubestore): name the limit that refused the message, not always the message one tungstenite raises the same capacity error for a frame over max_frame_size as for a message over max_message_size, and the variant doesn't say which. The reason hardcoded "maximum message size", so with CUBESTORE_TRANSPORT_MAX_FRAME_SIZE below the message limit a request that tripped the frame backstop was reported against a message limit that would have accepted it -- sending an operator to raise a knob that changes nothing. Compare the configured value back against both and name the one that matches. Equal limits, the default, keep naming the message one, where the number is the same either way. The neutral wording of naming both knobs doesn't fit: a close reason is capped at 123 bytes by RFC 6455 and the two variable names alone are most of that. New test configures the frame limit below the message limit and asserts the frame limit is what the close reports; the two existing size tests set them equal, so neither covered this. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GPf8PuEMdZ6RE7BusNGWej * fix(cubestore-driver): let the close reason replace the generic wording The reason was appended after the sentence it supersedes, so the error stated the same thing three times, and the ". " join assumed the peer's string carries no trailing full stop -- true of Cube Store's reason today, but the sibling handler string grew one two commits ago. Use the reason in place of the generic clause when there is one, folding out control characters since peer text lands in an error a user reads, and normalising the trailing full stop rather than assuming its absence. Also name both transport knobs in the advice. Now that Cube Store reports whichever limit refused the message, the reason can say "frame size" while the advice pointed only at CUBESTORE_TRANSPORT_MAX_MESSAGE_SIZE -- the knob that would not have helped. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GPf8PuEMdZ6RE7BusNGWej * test(cubestore-driver): cover a 1009 close that names the frame limit Both 1009 tests used a reason naming the message limit, so neither pinned the case the advice was widened for: a close reason quoting CUBESTORE_TRANSPORT_MAX_FRAME_SIZE must not be followed by advice pointing only at the message knob. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GPf8PuEMdZ6RE7BusNGWej --------- Co-authored-by: Claude --- .../cube-store-architecture.mdx | 23 +- .../configuration/environment-variables.mdx | 16 +- packages/cubejs-backend-shared/src/env.ts | 13 + packages/cubejs-cubestore-driver/package.json | 5 +- .../src/WebSocketConnection.ts | 208 ++++++-- .../cubejs-cubestore-driver/src/errors.ts | 13 + .../test/mock-cubestore-server.ts | 164 +++++++ .../test/websocket-connection.test.ts | 457 ++++++++++++++++++ rust/cubestore/cubestore/src/http/mod.rs | 382 ++++++++++++++- 9 files changed, 1243 insertions(+), 38 deletions(-) create mode 100644 packages/cubejs-cubestore-driver/test/mock-cubestore-server.ts create mode 100644 packages/cubejs-cubestore-driver/test/websocket-connection.test.ts diff --git a/docs-mintlify/docs/pre-aggregations/cube-store-architecture.mdx b/docs-mintlify/docs/pre-aggregations/cube-store-architecture.mdx index 4e7cac725e350..c605ddef19f24 100644 --- a/docs-mintlify/docs/pre-aggregations/cube-store-architecture.mdx +++ b/docs-mintlify/docs/pre-aggregations/cube-store-architecture.mdx @@ -1018,13 +1018,26 @@ A cache entry at or above `CUBESTORE_CACHE_MAX_ENTRY_SIZE` is rejected explicitl with `Unable to SET cache with '' key, exceeds maximum allowed size for payload: , max allowed: `. - +A message above the *transport* limit is answered with an error naming the +size and the limit, and the connection stays up for the other queries +multiplexed over it: -**Known limitation.** A message above the *transport* limit is not answered -with an error. The deployment then reports `ConnectionError: CubeStore connection error: -write EPIPE`. +``` +Request of 70000000 bytes exceeds the maximum message size of 67108864 bytes. +Reduce the size of the query, e.g. by sending fewer or smaller inline tables, +or raise CUBESTORE_TRANSPORT_MAX_MESSAGE_SIZE. +``` + +That holds up to twice whichever of the two transport limits above is lower. +A message beyond it is refused before it can be attributed to a query, so the +connection is closed instead and the other queries multiplexed over it are +re-sent on a new one. The close names the limit that refused it, so it says +which of the two to raise. - +A result too large for Cube's own limit is reported the same way, as +`Cube Store response size exceeds the maximum message size of 100 MB`. That +limit is [`CUBEJS_CUBESTORE_MAX_MESSAGE_SIZE`](/reference/configuration/environment-variables#cubejs_cubestore_max_message_size), +which is independent of the Cube Store side and defaults to 100 MB. If a query hits this limit, reduce the size of its result set — add filters or a lower `limit`, drop dimensions, or split it into several queries. diff --git a/docs-mintlify/reference/configuration/environment-variables.mdx b/docs-mintlify/reference/configuration/environment-variables.mdx index b5856225db9ec..9dd1c9fd20256 100644 --- a/docs-mintlify/reference/configuration/environment-variables.mdx +++ b/docs-mintlify/reference/configuration/environment-variables.mdx @@ -184,6 +184,20 @@ The port of the Cube Store deployment. | ------------------- | ---------------------- | --------------------- | | A valid port number | `3030` | `3030` | +## `CUBEJS_CUBESTORE_MAX_MESSAGE_SIZE` + +The maximum size of a single message Cube will send to or accept from Cube +Store. A result above this size is reported as `Cube Store response size +exceeds the maximum message size`; see +[message size limits](/docs/pre-aggregations/cube-store-architecture#message-size-limits). +This is Cube's own limit and is independent of +[`CUBESTORE_TRANSPORT_MAX_MESSAGE_SIZE`](#cubestore_transport_max_message_size), +which Cube Store applies to what it receives. + +| Possible Values | Default in Development | Default in Production | +| -------------------- | ---------------------- | --------------------- | +| A size in bytes | `104857600` (100 MB) | `104857600` (100 MB) | + ## `CUBEJS_QUEUE_FAST_TRACK` @@ -2149,7 +2163,7 @@ If `true`, then sends telemetry to Cube. ## `CUBESTORE_TRANSPORT_MAX_MESSAGE_SIZE` The maximum size of a single message on the WebSocket connection between Cube -and Cube Store. Messages above this size close the connection; see +and Cube Store. Messages above this size are answered with an error; see [message size limits](/docs/pre-aggregations/cube-store-architecture#message-size-limits). | Possible Values | Default in Development | Default in Production | diff --git a/packages/cubejs-backend-shared/src/env.ts b/packages/cubejs-backend-shared/src/env.ts index bf3acf56d4b65..158707e6948d4 100644 --- a/packages/cubejs-backend-shared/src/env.ts +++ b/packages/cubejs-backend-shared/src/env.ts @@ -1901,6 +1901,19 @@ const variables: Record any> = { cubeStoreNoHeartBeatTimeout: () => get('CUBEJS_CUBESTORE_NO_HEART_BEAT_TIMEOUT') .default('30') .asInt(), + /** + * Maximum size in bytes of a single message exchanged with Cube Store, both + * of a query sent to it and of a response received from it. + * + * It is the only limit that applies to responses, since Cube Store doesn't + * cap what it sends. For queries it is independent of, and by default looser + * than, CUBESTORE_TRANSPORT_MAX_MESSAGE_SIZE (64 MB), which is what Cube + * Store itself accepts: a query over that but under this one is refused by + * Cube Store rather than by this limit. + */ + cubeStoreMaxMessageSize: () => get('CUBEJS_CUBESTORE_MAX_MESSAGE_SIZE') + .default(String(100 * 1024 * 1024)) + .asIntPositive(), cubeStoreRollingWindowJoin: () => get('CUBEJS_CUBESTORE_ROLLING_WINDOW_JOIN') .default('true') .asBoolStrict(), diff --git a/packages/cubejs-cubestore-driver/package.json b/packages/cubejs-cubestore-driver/package.json index 8cb61a624913d..aeba408acd1ee 100644 --- a/packages/cubejs-cubestore-driver/package.json +++ b/packages/cubejs-cubestore-driver/package.json @@ -22,8 +22,9 @@ "build": "rm -rf dist && npm run tsc", "tsc": "tsc", "watch": "tsc -w", - "lint": "eslint src/*.ts", - "lint:fix": "eslint --fix src/*.ts" + "lint": "eslint src/*.ts test/*.ts", + "lint:fix": "eslint --fix src/*.ts test/*.ts", + "unit": "jest --coverage" }, "dependencies": { "@cubejs-backend/base-driver": "1.7.27", diff --git a/packages/cubejs-cubestore-driver/src/WebSocketConnection.ts b/packages/cubejs-cubestore-driver/src/WebSocketConnection.ts index 6e70d08b2378b..362fc11cf6ff8 100644 --- a/packages/cubejs-cubestore-driver/src/WebSocketConnection.ts +++ b/packages/cubejs-cubestore-driver/src/WebSocketConnection.ts @@ -4,7 +4,7 @@ import { v4 as uuidv4 } from 'uuid'; import { InlineTable } from '@cubejs-backend/base-driver'; import { getEnv, getProcessUid } from '@cubejs-backend/shared'; import { parseCubestoreResultMessage } from '@cubejs-backend/native'; -import { ConnectionError, QueryError } from './errors'; +import { ConnectionError, MessageTooLargeError, QueryError } from './errors'; import { BinaryValue, BoolValue, @@ -22,10 +22,33 @@ import { StringValue, } from '../codegen'; +// The WebSocket close code for a message that is too big to be processed: `ws` +// closes with it when an incoming message is over `maxPayload`, and a peer that +// refuses a message of ours is expected to close with it as well. +const MESSAGE_TOO_BIG_CLOSE_CODE = 1009; + +// The `ws` error code for an incoming message bigger than `maxPayload`. +const MAX_PAYLOAD_EXCEEDED_CODE = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'; + +function formatSize(bytes: number): string { + const units: [number, string][] = [[1024 * 1024, 'MB'], [1024, 'KB']]; + + for (const [unit, name] of units) { + if (bytes >= unit) { + return `${Math.round((bytes / unit) * 10) / 10} ${name}`; + } + } + + return `${bytes} bytes`; +} + interface SentMessage { resolve: (value: any) => void; reject: (reason?: any) => void; buffer: Uint8Array; + // How many connections died under this message from a failure that can't be + // attributed to a single message. Used to give it exactly one more round. + fatalRounds: number; } export type QueryParameter = null | boolean | number | string | Buffer; @@ -41,6 +64,9 @@ interface CubeStoreWebSocket extends WebSocket { lastHeartBeat: Date; sentMessages: Record; sendAsync: (message: Uint8Array) => Promise; + // A failure that killed this socket and that re-sending can't fix, so the + // message that caused it is rejected instead of being re-sent. + fatalError: Error | null; } export class WebSocketConnection { @@ -50,6 +76,8 @@ export class WebSocketConnection { protected readonly noHeartBeatTimeout: number; + protected readonly maxMessageSize: number; + protected currentConnectionTry: number; protected webSocket: CubeStoreWebSocket | null = null; @@ -65,6 +93,7 @@ export class WebSocketConnection { this.messageCounter = 1; this.maxConnectRetries = getEnv('cubeStoreMaxConnectRetries'); this.noHeartBeatTimeout = getEnv('cubeStoreNoHeartBeatTimeout'); + this.maxMessageSize = getEnv('cubeStoreMaxMessageSize'); this.currentConnectionTry = 0; this.connectionId = uuidv4(); } @@ -74,7 +103,7 @@ export class WebSocketConnection { const headers: Record = {}; headers['x-process-id'] = getProcessUid(); - const webSocket = new WebSocket(this.url, { headers }) as CubeStoreWebSocket; + const webSocket = new WebSocket(this.url, { headers, maxPayload: this.maxMessageSize }) as CubeStoreWebSocket; webSocket.on('upgrade', (response: any) => { this.cubeStoreVersion = response.headers['x-cubestore-version'] || null; }); @@ -91,23 +120,50 @@ export class WebSocketConnection { } }, 5000); - webSocket.sendAsync = async (message: Uint8Array) => new Promise((resolveSend, rejectSend) => { + webSocket.sendAsync = async (message: Uint8Array) => new Promise((resolveSend) => { // If socket is closing this message should be resent - if (webSocket.readyState === WebSocket.OPEN) { - webSocket.send(message, (err) => { - if (err) { - rejectSend(new ConnectionError( - `CubeStore connection error: ${err.message}`, - err - )); - } else { - resolveSend(); - } - }); + if (webSocket.readyState !== WebSocket.OPEN) { + resolveSend(); + return; } + + webSocket.send(message, (err) => { + if (err) { + // The write failed (EPIPE/ECONNRESET when Cube Store dropped the + // connection). The message stays registered in `sentMessages` and + // terminating gets 'close' to re-send it over a new connection -- + // failing it here would surface a spurious `write EPIPE` for a + // query that never reached Cube Store. + webSocket.terminate(); + } + + resolveSend(); + }); }); webSocket.on('open', () => resolve(webSocket)); webSocket.on('error', (err) => { + if ((err as any).code === MAX_PAYLOAD_EXCEEDED_CODE) { + // Cube Store answered with a message bigger than this connection + // accepts, and `ws` is tearing the connection down. Neither + // reconnecting nor retrying the query helps: the response would be + // just as big. Pending messages are rejected by the 'close' handler. + webSocket.fatalError = new MessageTooLargeError( + `Cube Store response size exceeds the maximum message size of ${formatSize(this.maxMessageSize)}. ` + + 'Reduce the amount of data the query returns, e.g. by adding filters or a limit, ' + + 'or raise CUBEJS_CUBESTORE_MAX_MESSAGE_SIZE.', + err + ); + + if (webSocket === this.webSocket) { + this.webSocket = null; + } + + // No-op if the connection was already established. + reject(webSocket.fatalError); + + return; + } + this.currentConnectionTry += 1; if (this.currentConnectionTry < this.maxConnectRetries) { @@ -131,16 +187,93 @@ export class WebSocketConnection { } webSocket.lastHeartBeat = new Date(); }); - webSocket.on('close', () => { + webSocket.on('close', (code: number, reason: Buffer) => { clearInterval(pingInterval); - if (Object.keys(webSocket.sentMessages).length) { + const pending = Object.keys(webSocket.sentMessages); + + if (pending.length) { + // Cube Store names the size and the limit that refused it here, + // which is strictly better than the generic wording, so it + // replaces it rather than being appended to it. Peer-supplied + // text lands in an error a user reads, so control characters are + // folded out and the trailing full stop is normalised rather than + // assumed absent. A peer that closes with 1009 and no reason -- + // an intermediary, or an older Cube Store -- keeps the generic + // wording, which is the only reason it still exists. + const closeReason = reason?.length + // eslint-disable-next-line no-control-regex + ? `${reason}`.replace(/[\u0000-\u001F\u007F]+/g, ' ').replace(/\s*\.?\s*$/, '') + : ''; + const fatalError = webSocket.fatalError || ( + // Cube Store refused a message that didn't fit into its limits. + code === MESSAGE_TOO_BIG_CLOSE_CODE ? new MessageTooLargeError( + `Cube Store closed the connection: ${closeReason || 'message size exceeds the maximum message size Cube Store accepts'}. ` + + 'Reduce the size of the query and of the inline tables it sends, or raise ' + + 'CUBESTORE_TRANSPORT_MAX_MESSAGE_SIZE or CUBESTORE_TRANSPORT_MAX_FRAME_SIZE ' + + 'on the Cube Store side.' + ) : null + ); + + // The connection multiplexes messages and an oversized one can't be + // attributed -- `ws` drops the frame before its message id is read + // -- so every message in flight gets one more round, which answers + // the innocent ones and usually leaves the offender alone to be + // named next time. That includes a message that was alone in + // flight: `fatalError` says an oversized frame was seen on this + // socket, not which query produced it, and it can be the response + // to a query rejected on an earlier round that was still on the + // wire. Whatever is still in flight after its round is failed + // regardless: an offender whose response keeps arriving before the + // other answers would otherwise be re-sent forever. + if (fatalError) { + // eslint-disable-next-line no-restricted-syntax + for (const key of pending) { + const sentMessage = webSocket.sentMessages[key]; + sentMessage.fatalRounds += 1; + + if (sentMessage.fatalRounds > 1) { + delete webSocket.sentMessages[key]; + sentMessage.reject(fatalError); + } + } + + if (!Object.keys(webSocket.sentMessages).length) { + if (webSocket === this.webSocket) { + this.webSocket = null; + } + + return; + } + } else { + // Only consecutive unattributable failures count towards giving + // up on a message: a query that outlived an ordinary disconnect + // gets its extra round back, so a later, unrelated oversized + // response can't fail it on the spot. The loop the counter + // bounds is fatal every round, so nothing resets there. + // eslint-disable-next-line no-restricted-syntax + for (const key of pending) { + webSocket.sentMessages[key].fatalRounds = 0; + } + } + setTimeout(async () => { try { const nextWebSocket = await this.initWebSocket(); + const resent = Object.keys(webSocket.sentMessages); + + // Register the whole batch before writing any of it. Writing + // yields, and a socket that closes in between must find every + // message of the batch: the ones not registered yet would end + // up on a socket whose 'close' has already been handled, with + // nobody left to write or to re-send them. // eslint-disable-next-line no-restricted-syntax - for (const key of Object.keys(webSocket.sentMessages)) { + for (const key of resent) { nextWebSocket.sentMessages[key] = webSocket.sentMessages[key]; + } + + // eslint-disable-next-line no-restricted-syntax + for (const key of resent) { await nextWebSocket.sendAsync(webSocket.sentMessages[key].buffer); } } catch (e) { @@ -182,6 +315,7 @@ export class WebSocketConnection { }); webSocket.sentMessages = {}; + webSocket.fatalError = null; this.webSocket = webSocket; } @@ -193,25 +327,43 @@ export class WebSocketConnection { } private async sendMessage(messageId: number, buffer: Uint8Array): Promise { + if (buffer.length > this.maxMessageSize) { + // Cube Store would close the connection on such a message, which shows up + // as an unrelated `write EPIPE`, so report it before sending anything. + // This only catches what is over our own limit: Cube Store applies its + // own, by default stricter, CUBESTORE_TRANSPORT_MAX_MESSAGE_SIZE, and a + // message it refuses is reported once it closes the connection. + throw new MessageTooLargeError( + `Cube Store request size of ${formatSize(buffer.length)} exceeds the maximum message size of ` + + `${formatSize(this.maxMessageSize)}. Reduce the size of the query and of the inline tables it sends, ` + + 'or raise CUBEJS_CUBESTORE_MAX_MESSAGE_SIZE together with CUBESTORE_TRANSPORT_MAX_MESSAGE_SIZE ' + + 'on the Cube Store side.' + ); + } + const socket = await this.initWebSocket(); return new Promise((resolve, reject) => { + socket.sentMessages[messageId] = { resolve, reject, buffer, fatalRounds: 0 }; + + // If socket is closing this message should be resent if (socket.readyState === WebSocket.OPEN) { socket.send(buffer, (err) => { if (err) { - delete socket.sentMessages[messageId]; - reject(new ConnectionError( - `CubeStore connection error: ${err.message}`, - err - )); + // Leave the message registered and let 'close' re-send it over a + // new connection instead of failing it with the write error. + socket.terminate(); } }); + } else if (socket.readyState === WebSocket.CLOSED) { + // 'close' already fired for this socket, so no re-send is going to pick + // this message up and nothing would ever settle it. That handler also + // dropped `this.webSocket`, so trying again establishes a fresh + // connection rather than failing a query that was never written. The + // `messageId` is deliberately kept, like a re-send: Cube Store + // de-duplicates on `(connection_id, message_id)`. + delete socket.sentMessages[messageId]; + this.sendMessage(messageId, buffer).then(resolve, reject); } - - socket.sentMessages[messageId] = { - resolve, - reject, - buffer - }; }); } diff --git a/packages/cubejs-cubestore-driver/src/errors.ts b/packages/cubejs-cubestore-driver/src/errors.ts index df9f52529a375..f0713c42d2c19 100644 --- a/packages/cubejs-cubestore-driver/src/errors.ts +++ b/packages/cubejs-cubestore-driver/src/errors.ts @@ -13,6 +13,19 @@ export class ConnectionError extends CubeStoreError { } } +/** + * A message didn't fit into the size limit of the connection. Unlike other + * connection errors this one is not worth retrying: the same message would be + * rejected again. + */ +export class MessageTooLargeError extends ConnectionError { + public constructor(message: string, cause?: Error) { + super(message, cause); + + this.name = 'MessageTooLargeError'; + } +} + export class QueryError extends CubeStoreError { public constructor(message: string) { super(message); diff --git a/packages/cubejs-cubestore-driver/test/mock-cubestore-server.ts b/packages/cubejs-cubestore-driver/test/mock-cubestore-server.ts new file mode 100644 index 0000000000000..f22121d545a52 --- /dev/null +++ b/packages/cubejs-cubestore-driver/test/mock-cubestore-server.ts @@ -0,0 +1,164 @@ +import { AddressInfo, Socket } from 'net'; +import * as flatbuffers from 'flatbuffers'; +import WebSocket from 'ws'; + +import { + HttpCommand, + HttpError, + HttpMessage, + HttpQuery, + HttpQueryResult, + HttpQueryResultArrow, + HttpQueryResultData, +} from '../codegen'; + +export interface ReceivedMessage { + connectionIndex: number; + messageId: number; + query: string; +} + +export interface MockConnection { + index: number; + ws: WebSocket; + socket: Socket; +} + +export type MessageHandler = (message: ReceivedMessage, connection: MockConnection) => void; + +/** + * Cube Store answers a query either with a result set or with an error. Tests + * use the error variant, because it's the only answer that can be asserted + * without the native result parser, and it's enough to tell "the query reached + * Cube Store and was answered" from "the query failed on the transport". + */ +export function buildErrorMessage(messageId: number, error: string): Buffer { + const builder = new flatbuffers.Builder(1024); + const errorOffset = builder.createString(error); + const commandOffset = HttpError.createHttpError(builder, errorOffset); + const message = HttpMessage.createHttpMessage(builder, messageId, HttpCommand.HttpError, commandOffset, 0); + builder.finish(message); + + return Buffer.from(builder.asUint8Array()); +} + +/** + * A successful answer. Its payload is decoded by the native result parser, so + * tests that use it stub that parser out. + */ +export function buildResultMessage(messageId: number, data: Buffer = Buffer.alloc(0)): Buffer { + const builder = new flatbuffers.Builder(1024); + const dataOffset = HttpQueryResultArrow.createDataVector(builder, data); + const arrowOffset = HttpQueryResultArrow.createHttpQueryResultArrow(builder, dataOffset, true); + const commandOffset = HttpQueryResult.createHttpQueryResult( + builder, + HttpQueryResultData.HttpQueryResultArrow, + arrowOffset + ); + const message = HttpMessage.createHttpMessage(builder, messageId, HttpCommand.HttpQueryResult, commandOffset, 0); + builder.finish(message); + + return Buffer.from(builder.asUint8Array()); +} + +export function answeredBy(connectionIndex: number): string { + return `answered by connection #${connectionIndex}`; +} + +/** + * A minimal Cube Store look-alike: it speaks the same WebSocket + flatbuffers + * protocol, so the driver talks to it over real TCP sockets, which can then be + * broken in the exact ways a real Cube Store restart breaks them. + */ +export class MockCubeStoreServer { + public readonly connections: MockConnection[] = []; + + public readonly received: ReceivedMessage[] = []; + + /** + * Replies to every query with an error naming the connection that received + * it, so a test can tell which connection answered. Can be replaced to + * emulate a Cube Store that goes away instead of answering. + */ + public handler: MessageHandler = (message, connection) => { + connection.ws.send(buildErrorMessage(message.messageId, answeredBy(connection.index))); + }; + + protected constructor(protected readonly wss: WebSocket.Server) { + wss.on('connection', (ws: WebSocket, request: any) => { + const connection: MockConnection = { + index: this.connections.length, + ws, + socket: request.socket, + }; + this.connections.push(connection); + + ws.on('message', (raw: Buffer) => { + const httpMessage = HttpMessage.getRootAsHttpMessage(new flatbuffers.ByteBuffer(raw)); + const message: ReceivedMessage = { + connectionIndex: connection.index, + messageId: httpMessage.messageId(), + query: httpMessage.command(new HttpQuery())?.query() || '', + }; + this.received.push(message); + this.handler(message, connection); + }); + // Connections are torn down by the tests on purpose, nothing to report. + ws.on('error', () => { + // noop + }); + }); + } + + public static async start(): Promise { + const wss = new WebSocket.Server({ host: '127.0.0.1', port: 0 }); + await new Promise((resolve, reject) => { + wss.once('listening', resolve); + wss.once('error', reject); + }); + + return new MockCubeStoreServer(wss); + } + + public get url(): string { + return `ws://127.0.0.1:${(this.wss.address() as AddressInfo).port}`; + } + + public connection(index: number): MockConnection { + if (!this.connections[index]) { + throw new Error(`Connection #${index} was never established`); + } + + return this.connections[index]; + } + + public async waitForConnections(count: number, timeout: number = 20000): Promise { + await this.waitFor(() => this.connections.length >= count, timeout, `${count} connection(s)`); + } + + public async waitForMessages(count: number, timeout: number = 20000): Promise { + await this.waitFor(() => this.received.length >= count, timeout, `${count} message(s)`); + } + + protected async waitFor(condition: () => boolean, timeout: number, description: string): Promise { + const deadline = Date.now() + timeout; + + while (!condition()) { + if (Date.now() > deadline) { + throw new Error(`Timed out waiting for ${description}`); + } + + await new Promise((resolve) => { setTimeout(resolve, 25); }); + } + } + + public async stop(): Promise { + for (const connection of this.connections) { + connection.ws.terminate(); + } + + await new Promise((resolve) => { + this.wss.close(() => resolve()); + }); + } +} diff --git a/packages/cubejs-cubestore-driver/test/websocket-connection.test.ts b/packages/cubejs-cubestore-driver/test/websocket-connection.test.ts new file mode 100644 index 0000000000000..33c51c6867249 --- /dev/null +++ b/packages/cubejs-cubestore-driver/test/websocket-connection.test.ts @@ -0,0 +1,457 @@ +import { Socket } from 'net'; + +import { WebSocketConnection } from '../src/WebSocketConnection'; +import { MessageTooLargeError, QueryError } from '../src/errors'; +import { QueryResultFormat } from '../codegen'; +import { + answeredBy, + buildErrorMessage, + buildResultMessage, + MockConnection, + MockCubeStoreServer, +} from './mock-cubestore-server'; + +const QUERY_RESULT = [{ answer: 42 }]; + +// Decoding a result set is a native addon and is orthogonal to the transport +// under test, so only that step is stubbed: the socket, the WebSocket framing +// and the flatbuffers protocol stay real. +jest.mock('@cubejs-backend/native', () => ({ + parseCubestoreResultMessage: jest.fn(async () => [{ answer: 42 }]), +})); + +const JEST_TIMEOUT = 60 * 1000; + +/** + * The error Node hands to a pending write when the peer is gone, as seen in + * `ConnectionError: CubeStore connection error: write EPIPE`. + */ +const epipe = () => Object.assign(new Error('write EPIPE'), { + code: 'EPIPE', + errno: -32, + syscall: 'write', +}); + +/** + * Waits until the frame the driver just handed to `ws` has reached the socket + * write buffer, where a corked socket holds it. + */ +const waitForBufferedWrite = async (socket: Socket) => { + const deadline = Date.now() + 5000; + + while (!socket.writableLength) { + if (Date.now() > deadline) { + throw new Error('Timed out waiting for a buffered write'); + } + + await new Promise((resolve) => { setImmediate(resolve); }); + } +}; + +describe('WebSocketConnection', () => { + let server: MockCubeStoreServer; + let connection: WebSocketConnection | null = null; + + beforeEach(async () => { + server = await MockCubeStoreServer.start(); + connection = null; + }); + + afterEach(async () => { + connection?.close(); + await server.stop(); + }); + + const query = (sql: string) => connection!.query(sql, [], { responseFormat: QueryResultFormat.Legacy }); + + /** + * The mock answers every query with an error naming the connection that + * served it, so a rejection carrying that marker means the query made a full + * round trip. A rejection with anything else (`ConnectionError: ... write + * EPIPE`) means the driver dropped the query instead of delivering it. + */ + const expectAnsweredBy = async (promise: Promise, connectionIndex: number) => { + await expect(promise).rejects.toThrow(QueryError); + await expect(promise).rejects.toThrow(answeredBy(connectionIndex)); + }; + + // The socket the driver is writing to right now. + const clientSocket = (): Socket => (connection as any).webSocket._socket; + + it('resolves a query with the result Cube Store sent', async () => { + connection = new WebSocketConnection(server.url); + + server.handler = (message, mockConnection) => { + mockConnection.ws.send(buildResultMessage(message.messageId)); + }; + + await expect(query('SELECT 1')).resolves.toEqual(QUERY_RESULT); + + // And the same after the connection had to be re-established mid-query. + const socket = clientSocket(); + socket.cork(); + const promise = query('SELECT 2'); + await waitForBufferedWrite(socket); + socket.destroy(epipe()); + + await expect(promise).resolves.toEqual(QUERY_RESULT); + expect(server.received.map((message) => message.query)).toEqual(['SELECT 1', 'SELECT 2']); + }, JEST_TIMEOUT); + + it('resends a query when the write fails with EPIPE', async () => { + connection = new WebSocketConnection(server.url); + + // Establish the connection with a first, successfully answered query. + await expectAnsweredBy(query('SELECT 1'), 0); + + // Keep the outgoing frame in the socket write buffer, then break the socket + // the way Node does once Cube Store is gone: the buffered write fails with + // EPIPE while `ws` still reports the connection as OPEN. + const socket = clientSocket(); + socket.cork(); + const promise = query('SELECT 2'); + await waitForBufferedWrite(socket); + socket.destroy(epipe()); + + // The query never reached Cube Store, so it has to be resent over a new + // connection instead of failing with the write error. + await expectAnsweredBy(promise, 1); + + expect(server.received.map((message) => [message.connectionIndex, message.query])).toEqual([ + [0, 'SELECT 1'], + [1, 'SELECT 2'], + ]); + }, JEST_TIMEOUT); + + it('resends every query that was in flight when the write failed', async () => { + connection = new WebSocketConnection(server.url); + + await expectAnsweredBy(query('SELECT 1'), 0); + + const socket = clientSocket(); + socket.cork(); + const promises = [query('SELECT 2'), query('SELECT 3'), query('SELECT 4')]; + await waitForBufferedWrite(socket); + socket.destroy(epipe()); + + await Promise.all(promises.map((promise) => expectAnsweredBy(promise, 1))); + + expect( + server.received.filter((message) => message.connectionIndex === 1).map((message) => message.query).sort() + ).toEqual(['SELECT 2', 'SELECT 3', 'SELECT 4']); + }, JEST_TIMEOUT); + + it('resends a query when the socket is no longer writable', async () => { + connection = new WebSocketConnection(server.url); + + await expectAnsweredBy(query('SELECT 1'), 0); + + // Half-close the socket: `ws` still reports OPEN, but the write fails. + clientSocket().end(); + + await expectAnsweredBy(query('SELECT 2'), 1); + }, JEST_TIMEOUT); + + it('resends a query when Cube Store closes the connection without answering', async () => { + connection = new WebSocketConnection(server.url); + + server.handler = (message, mockConnection) => { + if (mockConnection.index === 0) { + // Cube Store went away in the middle of the query. + mockConnection.ws.close(); + return; + } + + mockConnection.ws.send(buildErrorMessage(message.messageId, answeredBy(mockConnection.index))); + }; + + await expectAnsweredBy(query('SELECT 1'), 1); + }, JEST_TIMEOUT); + + describe('message size limit', () => { + const MAX_MESSAGE_SIZE = 1024 * 1024; + + beforeEach(() => { + process.env.CUBEJS_CUBESTORE_MAX_MESSAGE_SIZE = String(MAX_MESSAGE_SIZE); + }); + + afterEach(() => { + delete process.env.CUBEJS_CUBESTORE_MAX_MESSAGE_SIZE; + }); + + it('reports a response that is over the limit once its extra round is spent', async () => { + connection = new WebSocketConnection(server.url); + + server.handler = (message, mockConnection) => { + mockConnection.ws.send(Buffer.alloc(MAX_MESSAGE_SIZE * 2)); + }; + + const promise = query('SELECT 1'); + + await expect(promise).rejects.toThrow(MessageTooLargeError); + await expect(promise).rejects.toThrow( + 'Cube Store response size exceeds the maximum message size of 1 MB. ' + + 'Reduce the amount of data the query returns, e.g. by adding filters or a limit, ' + + 'or raise CUBEJS_CUBESTORE_MAX_MESSAGE_SIZE.' + ); + + // Re-sent once, since a fatal close on a sole in-flight query can just as + // well be an ordinary disconnect. The second oversized response spends + // its extra round and the size is reported rather than retried again. + expect(server.received).toHaveLength(2); + }, JEST_TIMEOUT); + + it('resends the other queries in flight and attributes the limit to the offender', async () => { + connection = new WebSocketConnection(server.url); + + // Cube Store answers the small query before it is done producing the big + // one. Ordering the two sends rather than spacing them apart in time + // keeps the test independent of how fast the driver is scheduled: the + // answer to the small query is on the wire, and therefore processed, + // before the oversized frame that tears the connection down. + const answeredSmall = new Set(); + const deferredBig = new Map(); + const sendOversized = (mockConnection: MockConnection) => { + mockConnection.ws.send(Buffer.alloc(MAX_MESSAGE_SIZE * 2)); + }; + + server.handler = (message, mockConnection) => { + if (message.query === 'SELECT big') { + // On the first connection the small query is left in flight, so that + // the oversized response kills it along with the query it belongs to. + if (mockConnection.index === 0 || answeredSmall.has(mockConnection.index)) { + sendOversized(mockConnection); + } else { + deferredBig.set(mockConnection.index, mockConnection); + } + + return; + } + + if (mockConnection.index > 0) { + mockConnection.ws.send(buildErrorMessage(message.messageId, answeredBy(mockConnection.index))); + answeredSmall.add(mockConnection.index); + + const deferred = deferredBig.get(mockConnection.index); + if (deferred) { + deferredBig.delete(mockConnection.index); + sendOversized(deferred); + } + } + }; + + const big = query('SELECT big'); + // Asserted below, handled here so that a rejection arriving earlier than + // expected is reported as a failed assertion and not as an unhandled one. + big.catch(() => { + // noop + }); + const small = query('SELECT small'); + + // The small query is unrelated to the size limit: it gets resent and + // answered rather than failing with an error about a limit it never + // approached. + await expectAnsweredBy(small, 1); + + // Which leaves the offending query alone on the connection, where the + // oversized response can be attributed to it. + await expect(big).rejects.toThrow(MessageTooLargeError); + await expect(big).rejects.toThrow('Cube Store response size exceeds the maximum message size of 1 MB'); + }, JEST_TIMEOUT); + + it('gives up when an over-limit response keeps killing the connection', async () => { + connection = new WebSocketConnection(server.url); + + // The oversized response always wins the race against the small query's + // answer, so re-sending never shrinks the set of messages in flight and + // never leaves the offender alone to be attributed. + const arrived = new Map>(); + + server.handler = (message, mockConnection) => { + const queries = arrived.get(mockConnection.index) || new Set(); + queries.add(message.query); + arrived.set(mockConnection.index, queries); + + if (queries.has('SELECT big') && queries.has('SELECT small')) { + mockConnection.ws.send(Buffer.alloc(MAX_MESSAGE_SIZE * 2)); + } + }; + + const big = query('SELECT big'); + const small = query('SELECT small'); + + // Both have to settle rather than being re-sent forever, even at the cost + // of blaming the size on a query that never approached the limit. + await expect(big).rejects.toThrow(MessageTooLargeError); + await expect(small).rejects.toThrow(MessageTooLargeError); + }, JEST_TIMEOUT); + + it('gives a query its extra round back once an ordinary disconnect intervenes', async () => { + connection = new WebSocketConnection(server.url); + + const arrived = new Map>(); + const longMessageIds = new Map(); + const sendOversized = (mockConnection: MockConnection) => { + mockConnection.ws.send(Buffer.alloc(MAX_MESSAGE_SIZE * 2)); + }; + + server.handler = (message, mockConnection) => { + const { index } = mockConnection; + const queries = arrived.get(index) || new Set(); + queries.add(message.query); + arrived.set(index, queries); + + if (message.query === 'SELECT long') { + longMessageIds.set(index, message.messageId); + } + + // Act once both are in flight, so the oversized response is never + // attributable to the query that caused it. + if (!queries.has('SELECT long') || !queries.has('SELECT big')) { + return; + } + + if (index === 1) { + // An ordinary disconnect, unrelated to message size. + mockConnection.ws.terminate(); + return; + } + + const longMessageId = longMessageIds.get(index); + if (index >= 3 && longMessageId !== undefined) { + // The slow query finally answers, which leaves the offender alone. + mockConnection.ws.send(buildErrorMessage(longMessageId, answeredBy(index))); + } + + sendOversized(mockConnection); + }; + + const long = query('SELECT long'); + const big = query('SELECT big'); + big.catch(() => { + // noop + }); + + // Two size incidents with an ordinary disconnect in between: the slow + // query is innocent in both, so the round it is owed has to survive the + // disconnect rather than being spent by the first incident. + await expectAnsweredBy(long, 3); + + await expect(big).rejects.toThrow(MessageTooLargeError); + }, JEST_TIMEOUT); + + it('reports a request that is over the limit without sending it', async () => { + connection = new WebSocketConnection(server.url); + + const promise = query(`SELECT ${'x'.repeat(MAX_MESSAGE_SIZE + 1)}`); + + await expect(promise).rejects.toThrow(MessageTooLargeError); + await expect(promise).rejects.toThrow( + /Cube Store request size of \d+(\.\d+)? MB exceeds the maximum message size of 1 MB/ + ); + + expect(server.connections).toHaveLength(0); + }, JEST_TIMEOUT); + + it('reports a request Cube Store refused as too big once its extra round is spent', async () => { + connection = new WebSocketConnection(server.url); + + server.handler = (message, mockConnection) => { + // How Cube Store rejects a message that doesn't fit into its limits, + // naming the size and the limit in the close reason. + mockConnection.ws.close( + 1009, + 'Message of 16452 bytes exceeds the maximum message size of 4096 bytes' + ); + }; + + const promise = query('SELECT 1'); + + await expect(promise).rejects.toThrow(MessageTooLargeError); + // The reason replaces the generic clause rather than being appended to + // it, so the sizes are stated once. + await expect(promise).rejects.toThrow( + 'Cube Store closed the connection: ' + + 'Message of 16452 bytes exceeds the maximum message size of 4096 bytes. ' + + 'Reduce the size of the query and of the inline tables it sends, or raise ' + + 'CUBESTORE_TRANSPORT_MAX_MESSAGE_SIZE or CUBESTORE_TRANSPORT_MAX_FRAME_SIZE ' + + 'on the Cube Store side.' + ); + + // Re-sent once before the size is reported, same as an over-limit + // response: one 1009 close is indistinguishable from a restart. + expect(server.received).toHaveLength(2); + }, JEST_TIMEOUT); + + it('falls back to the generic wording when 1009 carries no reason', async () => { + connection = new WebSocketConnection(server.url); + + // An intermediary, or a Cube Store from before it sent a reason. + server.handler = (message, mockConnection) => { + mockConnection.ws.close(1009); + }; + + const promise = query('SELECT 1'); + + await expect(promise).rejects.toThrow(MessageTooLargeError); + await expect(promise).rejects.toThrow( + 'Cube Store closed the connection: message size exceeds the maximum message size Cube Store accepts. ' + + 'Reduce the size of the query and of the inline tables it sends, or raise ' + + 'CUBESTORE_TRANSPORT_MAX_MESSAGE_SIZE or CUBESTORE_TRANSPORT_MAX_FRAME_SIZE ' + + 'on the Cube Store side.' + ); + }, JEST_TIMEOUT); + + it('does not double the full stop when 1009 carries a punctuated reason', async () => { + connection = new WebSocketConnection(server.url); + + server.handler = (message, mockConnection) => { + mockConnection.ws.close(1009, 'Message too big.'); + }; + + const promise = query('SELECT 1'); + + await expect(promise).rejects.toThrow( + 'Cube Store closed the connection: Message too big. Reduce the size of the query' + ); + }, JEST_TIMEOUT); + + it('does not send the user to the message limit when the frame limit refused it', async () => { + connection = new WebSocketConnection(server.url); + + // What Cube Store closes with when CUBESTORE_TRANSPORT_MAX_FRAME_SIZE is + // configured below the message limit and is the one that fired. + server.handler = (message, mockConnection) => { + mockConnection.ws.close( + 1009, + 'Message of 9437184 bytes exceeds the maximum frame size of 4194304 bytes' + ); + }; + + const promise = query('SELECT 1'); + + await expect(promise).rejects.toThrow( + 'Cube Store closed the connection: ' + + 'Message of 9437184 bytes exceeds the maximum frame size of 4194304 bytes. ' + + 'Reduce the size of the query and of the inline tables it sends, or raise ' + + 'CUBESTORE_TRANSPORT_MAX_MESSAGE_SIZE or CUBESTORE_TRANSPORT_MAX_FRAME_SIZE ' + + 'on the Cube Store side.' + ); + }, JEST_TIMEOUT); + }); + + it('rejects a query when the connection cannot be re-established', async () => { + process.env.CUBEJS_CUBESTORE_MAX_CONNECT_RETRIES = '2'; + + try { + const { url } = server; + await server.stop(); + + connection = new WebSocketConnection(url); + + await expect(query('SELECT 1')).rejects.toThrow('CubeStore connection failed after 2 retries'); + } finally { + delete process.env.CUBEJS_CUBESTORE_MAX_CONNECT_RETRIES; + } + }, JEST_TIMEOUT); +}); diff --git a/rust/cubestore/cubestore/src/http/mod.rs b/rust/cubestore/cubestore/src/http/mod.rs index 01fd4125bea1b..a1de965ea3b8f 100644 --- a/rust/cubestore/cubestore/src/http/mod.rs +++ b/rust/cubestore/cubestore/src/http/mod.rs @@ -33,6 +33,7 @@ use log::trace; use serde::Deserialize; use std::collections::HashMap; use std::convert::TryFrom; +use std::error::Error as StdError; use std::net::SocketAddr; use std::time::{Duration, SystemTime}; use tempfile::NamedTempFile; @@ -40,11 +41,85 @@ use tokio::fs::File; use tokio::io::{AsyncWriteExt, BufReader}; use tokio::sync::mpsc::Sender; use tokio::sync::{mpsc, Mutex}; +use tokio_tungstenite::tungstenite; use tokio_util::sync::CancellationToken; use warp::filters::ws::{Message, Ws}; use warp::http::StatusCode; use warp::reject::Reject; +/// Close code the WebSocket protocol reserves for a message a peer refuses to +/// process because it is too large (RFC 6455 section 7.4.1, "Message Too Big"). +const MESSAGE_TOO_BIG_CLOSE_CODE: u16 = 1009; + +/// How much room the transport is given above the configured sizes. +/// +/// The size limit is enforced here rather than by the transport, so that an +/// over-limit request can be answered with an error naming the message it +/// belongs to, on a connection that stays up for everything else multiplexed +/// over it. That needs the message to arrive whole: `tungstenite` raises its +/// capacity error as soon as the frame header is parsed, before the payload is +/// read, which leaves the frame stream desynchronized and the message id +/// unread. So the transport is configured a factor above the limit, and only +/// enforces it as a backstop against a peer that would otherwise make the +/// server buffer without bound. A message in between is what gets the readable +/// error; past the backstop there is nothing to answer with but a close frame. +/// +/// The frame limit is given the same headroom, and has to be: a client sends a +/// query as a single frame, so an exact frame limit would refuse an over-limit +/// message at the transport before the handler ever saw it, and at the default +/// configuration — where `CUBESTORE_TRANSPORT_MAX_FRAME_SIZE` and +/// `CUBESTORE_TRANSPORT_MAX_MESSAGE_SIZE` are both `64 << 20` — that is every +/// over-limit message, leaving the readable error unreachable. +/// +/// Frame size is not re-checked in the handler, because reassembly happens +/// below `warp` and individual frames are never visible up here. So with the +/// two knobs configured apart, `CUBESTORE_TRANSPORT_MAX_FRAME_SIZE` bounds a +/// single allocation at the headroom multiple of its configured value, and the +/// message limit is what actually enforces the policy. +const TRANSPORT_SIZE_HEADROOM: usize = 2; + +/// Recognizes the error raised when an incoming message exceeds +/// `CUBESTORE_TRANSPORT_MAX_MESSAGE_SIZE` or `CUBESTORE_TRANSPORT_MAX_FRAME_SIZE`, +/// and renders it as a close reason. `warp` boxes the underlying `tungstenite` +/// error, so it has to be recovered through `source()`. +fn message_too_large_reason( + e: &warp::Error, + max_message_size: usize, + max_frame_size: usize, +) -> Option { + match e.source()?.downcast_ref::()? { + tungstenite::Error::Capacity(tungstenite::error::CapacityError::MessageTooLong { + size, + max_size, + }) => { + // `max_size` is the backstop the transport was configured with, + // which is the configured limit times the headroom. Report what + // the operator actually set, since that is the number they can + // change. + let configured = max_size / TRANSPORT_SIZE_HEADROOM; + // The same error covers both caps and doesn't say which one + // fired, so the value has to name the knob: a frame limit + // configured below the message limit would otherwise be reported + // as a message limit that never refused anything, sending an + // operator to the wrong environment variable. When the two are + // equal, as they are by default, the number is the same either + // way and the message limit is the one to name. + let limit = if configured == max_message_size { + "message" + } else if configured == max_frame_size { + "frame" + } else { + "transport" + }; + Some(format!( + "Message of {} bytes exceeds the maximum {} size of {} bytes", + size, limit, configured + )) + } + _ => None, + } +} + pub struct HttpServer { bind_address: String, sql_service: Arc, @@ -170,7 +245,7 @@ impl HttpServer { .and_then(move |tx: mpsc::Sender<(mpsc::Sender>, SqlQueryContext, HttpMessage)>, sql_query_context: SqlQueryContext, ws: Ws| async move { let tx_to_move = tx.clone(); let sql_query_context = sql_query_context.clone(); - let reply = ws.max_frame_size(max_frame_size).max_message_size(max_message_size).on_upgrade(async move |mut web_socket| { + let reply = ws.max_frame_size(max_frame_size.saturating_mul(TRANSPORT_SIZE_HEADROOM)).max_message_size(max_message_size.saturating_mul(TRANSPORT_SIZE_HEADROOM)).on_upgrade(async move |mut web_socket| { let process_id = sql_query_context.process_id.as_deref().unwrap_or("None"); trace!("WebSocket connection established (process_id: {})", process_id); let (response_tx, mut response_rx) = mpsc::channel::>(10000); @@ -190,7 +265,27 @@ impl HttpServer { Some(msg) = web_socket.next() => { match msg { Err(e) => { - error!("Websocket error: {:?}", e); + // Past the transport backstop the payload is refused + // before it is read, so the frame stream is left + // desynchronized and the message id is never seen: the + // connection can't be reused and the error can't be + // attributed to a query the way an over-limit request + // under the backstop is. Answer with the close code + // reserved for this instead of dropping the connection + // silently, so the client can report the size rather than + // a bare disconnect it would otherwise retry. + match message_too_large_reason(&e, max_message_size, max_frame_size) { + Some(reason) => { + error!("Websocket message too large: {}", reason); + let send_res = web_socket.send( + Message::close_with(MESSAGE_TOO_BIG_CLOSE_CODE, reason) + ).await; + if let Err(e) = send_res { + error!("Websocket close send error: {:?}", e) + } + } + None => error!("Websocket error: {:?}", e), + } break; } Ok(msg) => { @@ -207,6 +302,25 @@ impl HttpServer { let message_id = http_message.message_id(); let connection_id = http_message.connection_id().map(|s| s.to_string()); + // Refused here rather than by the transport so the + // answer can name the message it belongs to and the + // connection survives for everything else in flight + // on it. See TRANSPORT_SIZE_HEADROOM. + if message_buffer.len() > max_message_size { + let error = format!( + "Request of {} bytes exceeds the maximum message size of {} bytes. Reduce the size of the query, e.g. by sending fewer or smaller inline tables, or raise CUBESTORE_TRANSPORT_MAX_MESSAGE_SIZE.", + message_buffer.len(), max_message_size + ); + error!("Websocket message too large: {}", error); + let send_res = web_socket.send( + Message::binary(HttpMessage { message_id, connection_id, command: HttpCommand::Error { error } }.bytes()) + ).await; + if let Err(e) = send_res { + error!("Websocket message send error: {:?}", e) + } + continue; + } + match HttpMessage::read(http_message).await { Err(e) => { error!("Websocket message read error: {:?}", e); @@ -1861,4 +1975,268 @@ mod tests { http_server.stop_processing().await; Ok(()) } + + /// An incoming message past the transport backstop is answered with the + /// WebSocket "message too big" close code instead of the connection being + /// dropped without a word, which the client can only read as a bare + /// disconnect and retry. + #[tokio::test] + async fn ws_message_too_large_test() -> Result<(), CubeError> { + init_test_logger().await; + + let max_message_size = 4 * 1024; + let mut auth = MockSqlAuthService::new(); + auth.expect_authenticate().return_const(Ok(None)); + + let http_server = Arc::new(HttpServer::new( + "127.0.0.1:53033".to_string(), + Arc::new(auth), + Arc::new(SqlServiceMock { + message_counter: AtomicU64::new(0), + }), + Duration::from_millis(100), + Duration::from_millis(10000), + Duration::from_millis(1000), + max_message_size, + max_message_size, + )); + { + let http_server = http_server.clone(); + cube_ext::spawn(async move { http_server.run_server().await }); + } + + tokio::time::sleep(Duration::from_secs(1)).await; + + let (mut socket, _) = connect_async(Url::parse("ws://127.0.0.1:53033/ws").unwrap()) + .await + .unwrap(); + + // Clear of the headroom the graceful path is given, so that the server + // refuses the frame before it can read the message id back out of it. + socket + .send(Message::binary( + HttpMessage { + message_id: 1, + command: HttpCommand::Query { + query: "s".repeat(max_message_size * TRANSPORT_SIZE_HEADROOM * 2), + inline_tables: vec![], + trace_obj: None, + parameters: None, + response_format: QueryResultFormat::Legacy, + }, + connection_id: Some("foo".to_string()), + } + .bytes(), + )) + .await + .unwrap(); + + let msg = socket.next().await.unwrap().unwrap(); + match msg { + Message::Close(Some(frame)) => { + assert_eq!(u16::from(frame.code), MESSAGE_TOO_BIG_CLOSE_CODE); + // The configured limit, not the backstop the transport is + // given: that is the number an operator set and can change. + assert!( + frame.reason.contains(&format!( + "exceeds the maximum message size of {} bytes", + max_message_size + )), + "unexpected close reason: {}", + frame.reason + ); + } + msg => panic!("Close frame expected, got: {:?}", msg), + } + + http_server.stop_processing().await; + Ok(()) + } + + /// The close reason names whichever limit refused the message. The same + /// capacity error covers both, so a frame limit configured below the + /// message limit would otherwise be reported as a message limit that never + /// refused anything. + #[tokio::test] + async fn ws_message_too_large_names_the_frame_limit_test() -> Result<(), CubeError> { + init_test_logger().await; + + let max_frame_size = 4 * 1024; + let max_message_size = max_frame_size * 4; + let mut auth = MockSqlAuthService::new(); + auth.expect_authenticate().return_const(Ok(None)); + + let http_server = Arc::new(HttpServer::new( + "127.0.0.1:53035".to_string(), + Arc::new(auth), + Arc::new(SqlServiceMock { + message_counter: AtomicU64::new(0), + }), + Duration::from_millis(100), + Duration::from_millis(10000), + Duration::from_millis(1000), + max_message_size, + max_frame_size, + )); + { + let http_server = http_server.clone(); + cube_ext::spawn(async move { http_server.run_server().await }); + } + + tokio::time::sleep(Duration::from_secs(1)).await; + + let (mut socket, _) = connect_async(Url::parse("ws://127.0.0.1:53035/ws").unwrap()) + .await + .unwrap(); + + // Past the frame backstop but inside the message one, so the frame + // limit is what refuses it. + socket + .send(Message::binary( + HttpMessage { + message_id: 1, + command: HttpCommand::Query { + query: "s".repeat(max_frame_size * TRANSPORT_SIZE_HEADROOM * 2), + inline_tables: vec![], + trace_obj: None, + parameters: None, + response_format: QueryResultFormat::Legacy, + }, + connection_id: Some("foo".to_string()), + } + .bytes(), + )) + .await + .unwrap(); + + let msg = socket.next().await.unwrap().unwrap(); + match msg { + Message::Close(Some(frame)) => { + assert_eq!(u16::from(frame.code), MESSAGE_TOO_BIG_CLOSE_CODE); + assert!( + frame.reason.contains(&format!( + "exceeds the maximum frame size of {} bytes", + max_frame_size + )), + "unexpected close reason: {}", + frame.reason + ); + } + msg => panic!("Close frame expected, got: {:?}", msg), + } + + http_server.stop_processing().await; + Ok(()) + } + + /// An over-limit request that still fits within the transport headroom is + /// answered with an error naming the message it belongs to, and the + /// connection carries on serving the queries multiplexed over it. + #[tokio::test] + async fn ws_message_too_large_reports_the_message_test() -> Result<(), CubeError> { + init_test_logger().await; + + let max_message_size = 4 * 1024; + let mut auth = MockSqlAuthService::new(); + auth.expect_authenticate().return_const(Ok(None)); + + let http_server = Arc::new(HttpServer::new( + "127.0.0.1:53034".to_string(), + Arc::new(auth), + Arc::new(SqlServiceMock { + message_counter: AtomicU64::new(0), + }), + Duration::from_millis(100), + Duration::from_millis(10000), + Duration::from_millis(1000), + max_message_size, + max_message_size, + )); + { + let http_server = http_server.clone(); + cube_ext::spawn(async move { http_server.run_server().await }); + } + + tokio::time::sleep(Duration::from_secs(1)).await; + + let (mut socket, _) = connect_async(Url::parse("ws://127.0.0.1:53034/ws").unwrap()) + .await + .unwrap(); + + // Over the limit, but inside the headroom the transport is given, so it + // arrives whole and can be attributed to message 7. + socket + .send(Message::binary( + HttpMessage { + message_id: 7, + command: HttpCommand::Query { + query: "s".repeat(max_message_size + max_message_size / 2), + inline_tables: vec![], + trace_obj: None, + parameters: None, + response_format: QueryResultFormat::Legacy, + }, + connection_id: Some("foo".to_string()), + } + .bytes(), + )) + .await + .unwrap(); + + // Read off the flatbuffer directly: `HttpMessage::read` only decodes the + // commands a client sends, and an error is not one of them. + let msg = socket.next().await.unwrap().unwrap(); + let data = msg.into_data(); + let message = root_as_http_message(&data).unwrap(); + assert_eq!(message.message_id(), 7); + let error = message + .command_as_http_error() + .expect("an error was expected") + .error() + .unwrap_or_default(); + assert!( + error.contains(&format!( + "exceeds the maximum message size of {} bytes", + max_message_size + )), + "unexpected error: {}", + error + ); + + // The point of answering instead of closing: the connection is still + // usable for everything that wasn't oversized. + socket + .send(Message::binary( + HttpMessage { + message_id: 8, + command: HttpCommand::Query { + query: "foo".to_string(), + inline_tables: vec![], + trace_obj: None, + parameters: None, + response_format: QueryResultFormat::Legacy, + }, + connection_id: Some("foo".to_string()), + } + .bytes(), + )) + .await + .unwrap(); + + let msg = socket.next().await.unwrap().unwrap(); + let message = HttpMessage::read(root_as_http_message(&msg.into_data()).unwrap()) + .await + .unwrap(); + assert_eq!(message.message_id, 8); + match message.command { + HttpCommand::ResultSet { data_frame } => assert_eq!( + data_frame.get_rows()[0].values()[0], + TableValue::String("0".to_string()) + ), + command => panic!("Result set expected, got: {:?}", command), + } + + http_server.stop_processing().await; + Ok(()) + } } From 8785332469eb076b497ee6b2eb678d77fa8a5618 Mon Sep 17 00:00:00 2001 From: Pavel Tiunov Date: Wed, 26 Aug 2026 16:15:39 -0700 Subject: [PATCH 5/5] v1.7.28 --- CHANGELOG.md | 11 ++++ lerna.json | 2 +- packages/cubejs-api-gateway/CHANGELOG.md | 4 ++ packages/cubejs-api-gateway/package.json | 10 ++-- packages/cubejs-athena-driver/CHANGELOG.md | 4 ++ packages/cubejs-athena-driver/package.json | 10 ++-- packages/cubejs-backend-cloud/CHANGELOG.md | 4 ++ packages/cubejs-backend-cloud/package.json | 6 +- packages/cubejs-backend-maven/CHANGELOG.md | 4 ++ packages/cubejs-backend-maven/package.json | 6 +- packages/cubejs-backend-native/CHANGELOG.md | 4 ++ packages/cubejs-backend-native/package.json | 8 +-- packages/cubejs-backend-shared/CHANGELOG.md | 6 ++ packages/cubejs-backend-shared/package.json | 4 +- packages/cubejs-base-driver/CHANGELOG.md | 4 ++ packages/cubejs-base-driver/package.json | 6 +- packages/cubejs-bigquery-driver/CHANGELOG.md | 4 ++ packages/cubejs-bigquery-driver/package.json | 8 +-- packages/cubejs-cli/CHANGELOG.md | 4 ++ packages/cubejs-cli/package.json | 12 ++-- .../cubejs-clickhouse-driver/CHANGELOG.md | 4 ++ .../cubejs-clickhouse-driver/package.json | 10 ++-- packages/cubejs-client-core/CHANGELOG.md | 4 ++ packages/cubejs-client-core/package.json | 4 +- packages/cubejs-client-dx/CHANGELOG.md | 4 ++ packages/cubejs-client-dx/package.json | 2 +- packages/cubejs-client-ngx/CHANGELOG.md | 4 ++ packages/cubejs-client-ngx/package.json | 2 +- packages/cubejs-client-react/CHANGELOG.md | 4 ++ packages/cubejs-client-react/package.json | 4 +- packages/cubejs-client-vue3/CHANGELOG.md | 4 ++ packages/cubejs-client-vue3/package.json | 4 +- .../cubejs-client-ws-transport/CHANGELOG.md | 4 ++ .../cubejs-client-ws-transport/package.json | 6 +- packages/cubejs-crate-driver/CHANGELOG.md | 4 ++ packages/cubejs-crate-driver/package.json | 10 ++-- packages/cubejs-cubestore-driver/CHANGELOG.md | 6 ++ packages/cubejs-cubestore-driver/package.json | 12 ++-- .../CHANGELOG.md | 4 ++ .../package.json | 12 ++-- .../cubejs-dbt-schema-extension/CHANGELOG.md | 4 ++ .../cubejs-dbt-schema-extension/package.json | 8 +-- packages/cubejs-docker/CHANGELOG.md | 4 ++ packages/cubejs-docker/package.json | 58 +++++++++---------- packages/cubejs-dremio-driver/CHANGELOG.md | 4 ++ packages/cubejs-dremio-driver/package.json | 12 ++-- packages/cubejs-druid-driver/CHANGELOG.md | 6 ++ packages/cubejs-druid-driver/package.json | 10 ++-- packages/cubejs-duckdb-driver/CHANGELOG.md | 4 ++ packages/cubejs-duckdb-driver/package.json | 12 ++-- packages/cubejs-firebolt-driver/CHANGELOG.md | 4 ++ packages/cubejs-firebolt-driver/package.json | 12 ++-- packages/cubejs-hive-driver/CHANGELOG.md | 4 ++ packages/cubejs-hive-driver/package.json | 8 +-- packages/cubejs-jdbc-driver/CHANGELOG.md | 4 ++ packages/cubejs-jdbc-driver/package.json | 8 +-- packages/cubejs-ksql-driver/CHANGELOG.md | 6 ++ packages/cubejs-ksql-driver/package.json | 10 ++-- packages/cubejs-linter/CHANGELOG.md | 4 ++ packages/cubejs-linter/package.json | 2 +- .../cubejs-materialize-driver/CHANGELOG.md | 4 ++ .../cubejs-materialize-driver/package.json | 12 ++-- packages/cubejs-mongobi-driver/CHANGELOG.md | 4 ++ packages/cubejs-mongobi-driver/package.json | 8 +-- packages/cubejs-mssql-driver/CHANGELOG.md | 4 ++ packages/cubejs-mssql-driver/package.json | 6 +- .../CHANGELOG.md | 4 ++ .../package.json | 8 +-- packages/cubejs-mysql-driver/CHANGELOG.md | 4 ++ packages/cubejs-mysql-driver/package.json | 10 ++-- packages/cubejs-oracle-driver/CHANGELOG.md | 4 ++ packages/cubejs-oracle-driver/package.json | 6 +- packages/cubejs-pinot-driver/CHANGELOG.md | 4 ++ packages/cubejs-pinot-driver/package.json | 10 ++-- packages/cubejs-playground/CHANGELOG.md | 4 ++ packages/cubejs-playground/package.json | 6 +- packages/cubejs-postgres-driver/CHANGELOG.md | 4 ++ packages/cubejs-postgres-driver/package.json | 10 ++-- packages/cubejs-prestodb-driver/CHANGELOG.md | 4 ++ packages/cubejs-prestodb-driver/package.json | 8 +-- .../cubejs-query-orchestrator/CHANGELOG.md | 4 ++ .../cubejs-query-orchestrator/package.json | 10 ++-- packages/cubejs-questdb-driver/CHANGELOG.md | 4 ++ packages/cubejs-questdb-driver/package.json | 12 ++-- packages/cubejs-redshift-driver/CHANGELOG.md | 4 ++ packages/cubejs-redshift-driver/package.json | 10 ++-- packages/cubejs-schema-compiler/CHANGELOG.md | 6 ++ packages/cubejs-schema-compiler/package.json | 12 ++-- packages/cubejs-server-core/CHANGELOG.md | 4 ++ packages/cubejs-server-core/package.json | 24 ++++---- packages/cubejs-server/CHANGELOG.md | 4 ++ packages/cubejs-server/package.json | 14 ++--- packages/cubejs-snowflake-driver/CHANGELOG.md | 4 ++ packages/cubejs-snowflake-driver/package.json | 8 +-- packages/cubejs-sqlite-driver/CHANGELOG.md | 4 ++ packages/cubejs-sqlite-driver/package.json | 8 +-- packages/cubejs-templates/CHANGELOG.md | 4 ++ packages/cubejs-templates/package.json | 6 +- packages/cubejs-testing-drivers/CHANGELOG.md | 4 ++ packages/cubejs-testing-drivers/package.json | 46 +++++++-------- packages/cubejs-testing-shared/CHANGELOG.md | 4 ++ packages/cubejs-testing-shared/package.json | 10 ++-- packages/cubejs-testing/CHANGELOG.md | 4 ++ packages/cubejs-testing/package.json | 22 +++---- packages/cubejs-trino-driver/CHANGELOG.md | 4 ++ packages/cubejs-trino-driver/package.json | 12 ++-- packages/cubejs-vertica-driver/CHANGELOG.md | 4 ++ packages/cubejs-vertica-driver/package.json | 14 ++--- rust/cubesql/CHANGELOG.md | 6 ++ rust/cubesql/package.json | 2 +- rust/cubestore/CHANGELOG.md | 6 ++ rust/cubestore/Cargo.lock | 2 +- rust/cubestore/cubestore/Cargo.toml | 2 +- rust/cubestore/package.json | 6 +- 114 files changed, 536 insertions(+), 291 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 847c4ea6d1f8f..194650103f508 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +### Bug Fixes + +- **cubestore-driver:** don't fail queries on `write EPIPE`, report over-limit messages readably ([#11490](https://github.com/cube-js/cube/issues/11490)) ([d9ff741](https://github.com/cube-js/cube/commit/d9ff741b991ae9fb0bde5b742f6d514f3a7f94d7)), closes [#11655](https://github.com/cube-js/cube/issues/11655) +- **release:** repair lerna-publish.sh's release step and bump cleanup ([#11658](https://github.com/cube-js/cube/issues/11658)) ([a3bc548](https://github.com/cube-js/cube/commit/a3bc548ea1fffacccf6c77477cfb835b7d83429e)) + +### Features + +- **cubesql:** Push UNION down to the data source ([#11651](https://github.com/cube-js/cube/issues/11651)) ([2aba3ac](https://github.com/cube-js/cube/commit/2aba3ac6d15b3f6885df952f3b0380dbbbd5a31d)) + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) ### Bug Fixes diff --git a/lerna.json b/lerna.json index ca0c93c498eb0..6dd0bd7ff55f7 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.7.27", + "version": "1.7.28", "npmClient": "yarn", "command": { "bootstrap": { diff --git a/packages/cubejs-api-gateway/CHANGELOG.md b/packages/cubejs-api-gateway/CHANGELOG.md index eabda1dd058a1..e48a266fb4417 100644 --- a/packages/cubejs-api-gateway/CHANGELOG.md +++ b/packages/cubejs-api-gateway/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/api-gateway + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/api-gateway diff --git a/packages/cubejs-api-gateway/package.json b/packages/cubejs-api-gateway/package.json index 38a1ce078dbff..5b3553858ac10 100644 --- a/packages/cubejs-api-gateway/package.json +++ b/packages/cubejs-api-gateway/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/api-gateway", "description": "Cube API Gateway", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -27,9 +27,9 @@ "dist/src/*" ], "dependencies": { - "@cubejs-backend/native": "1.7.27", - "@cubejs-backend/query-orchestrator": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/native": "1.7.28", + "@cubejs-backend/query-orchestrator": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "@ungap/structured-clone": "^0.3.4", "assert-never": "^1.4.0", "body-parser": "^1.19.0", @@ -53,7 +53,7 @@ "zod": "^4.1.13" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "@types/express": "^4.17.21", "@types/jest": "^29", "@types/jsonwebtoken": "^9.0.2", diff --git a/packages/cubejs-athena-driver/CHANGELOG.md b/packages/cubejs-athena-driver/CHANGELOG.md index dc78153be4acf..cc8ac76fe2c1d 100644 --- a/packages/cubejs-athena-driver/CHANGELOG.md +++ b/packages/cubejs-athena-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/athena-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/athena-driver diff --git a/packages/cubejs-athena-driver/package.json b/packages/cubejs-athena-driver/package.json index 43ee64c2999d7..1c49026cf01fb 100644 --- a/packages/cubejs-athena-driver/package.json +++ b/packages/cubejs-athena-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/athena-driver", "description": "Cube.js Athena database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -31,12 +31,12 @@ "dependencies": { "@aws-sdk/client-athena": "^3.22.0", "@aws-sdk/credential-providers": "^3.22.0", - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/shared": "1.7.27" + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/shared": "1.7.28" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", - "@cubejs-backend/testing-shared": "1.7.27", + "@cubejs-backend/linter": "1.7.28", + "@cubejs-backend/testing-shared": "1.7.28", "@types/ramda": "^0.27.40", "typescript": "~5.2.2" }, diff --git a/packages/cubejs-backend-cloud/CHANGELOG.md b/packages/cubejs-backend-cloud/CHANGELOG.md index c7c2acc9bd6df..54bd3a3d49f02 100644 --- a/packages/cubejs-backend-cloud/CHANGELOG.md +++ b/packages/cubejs-backend-cloud/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/cloud + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/cloud diff --git a/packages/cubejs-backend-cloud/package.json b/packages/cubejs-backend-cloud/package.json index fabf31b9f54a8..d6dca569fa3cb 100644 --- a/packages/cubejs-backend-cloud/package.json +++ b/packages/cubejs-backend-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@cubejs-backend/cloud", - "version": "1.7.27", + "version": "1.7.28", "description": "Cube Cloud package", "main": "dist/src/index.js", "typings": "dist/src/index.d.ts", @@ -30,7 +30,7 @@ "devDependencies": { "@babel/core": "^7.24.5", "@babel/preset-env": "^7.24.5", - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "@types/fs-extra": "^9.0.8", "@types/jest": "^29", "jest": "^29", @@ -38,7 +38,7 @@ }, "dependencies": { "@cubejs-backend/dotenv": "^9.0.2", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/shared": "1.7.28", "chokidar": "^3.5.1", "env-var": "^6.3.0", "form-data": "^4.0.0", diff --git a/packages/cubejs-backend-maven/CHANGELOG.md b/packages/cubejs-backend-maven/CHANGELOG.md index 408c7670cc0c7..83480ad76b2a3 100644 --- a/packages/cubejs-backend-maven/CHANGELOG.md +++ b/packages/cubejs-backend-maven/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/maven + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/maven diff --git a/packages/cubejs-backend-maven/package.json b/packages/cubejs-backend-maven/package.json index fc10cfae81702..af7564ecae986 100644 --- a/packages/cubejs-backend-maven/package.json +++ b/packages/cubejs-backend-maven/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/maven", "description": "Cube.js Maven Wrapper for java dependencies downloading", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "license": "Apache-2.0", "repository": { "type": "git", @@ -31,12 +31,12 @@ "dist/src/*" ], "dependencies": { - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/shared": "1.7.28", "source-map-support": "^0.5.19", "xmlbuilder2": "^2.4.0" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "@types/jest": "^29", "@types/node": "^22", "jest": "^29", diff --git a/packages/cubejs-backend-native/CHANGELOG.md b/packages/cubejs-backend-native/CHANGELOG.md index aed3734f9888d..ec097487f0957 100644 --- a/packages/cubejs-backend-native/CHANGELOG.md +++ b/packages/cubejs-backend-native/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/native + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) ### Bug Fixes diff --git a/packages/cubejs-backend-native/package.json b/packages/cubejs-backend-native/package.json index 6add5c1ec2237..c190847902b5e 100644 --- a/packages/cubejs-backend-native/package.json +++ b/packages/cubejs-backend-native/package.json @@ -1,6 +1,6 @@ { "name": "@cubejs-backend/native", - "version": "1.7.27", + "version": "1.7.28", "author": "Cube Dev, Inc.", "description": "Native module for Cube.js (binding to Rust codebase)", "main": "dist/js/index.js", @@ -39,7 +39,7 @@ "dist/js" ], "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "@types/jest": "^29", "@types/node": "^22", "cargo-cp-artifact": "^0.1.9", @@ -50,8 +50,8 @@ "uuid": "^11.1.1" }, "dependencies": { - "@cubejs-backend/cubesql": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/cubesql": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "@cubejs-infra/post-installer": "^0.1.2" }, "resources": { diff --git a/packages/cubejs-backend-shared/CHANGELOG.md b/packages/cubejs-backend-shared/CHANGELOG.md index 1eead5215a522..aafab09388ab8 100644 --- a/packages/cubejs-backend-shared/CHANGELOG.md +++ b/packages/cubejs-backend-shared/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +### Bug Fixes + +- **cubestore-driver:** don't fail queries on `write EPIPE`, report over-limit messages readably ([#11490](https://github.com/cube-js/cube/issues/11490)) ([d9ff741](https://github.com/cube-js/cube/commit/d9ff741b991ae9fb0bde5b742f6d514f3a7f94d7)), closes [#11655](https://github.com/cube-js/cube/issues/11655) + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/shared diff --git a/packages/cubejs-backend-shared/package.json b/packages/cubejs-backend-shared/package.json index 2e5a5144f9d9e..9bc2af1be01cf 100644 --- a/packages/cubejs-backend-shared/package.json +++ b/packages/cubejs-backend-shared/package.json @@ -1,6 +1,6 @@ { "name": "@cubejs-backend/shared", - "version": "1.7.27", + "version": "1.7.28", "description": "Shared code for Cube.js backend packages", "main": "dist/src/index.js", "typings": "dist/src/index.d.ts", @@ -27,7 +27,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "@types/bytes": "^3.1.5", "@types/cli-progress": "^3.9.1", "@types/jest": "^29", diff --git a/packages/cubejs-base-driver/CHANGELOG.md b/packages/cubejs-base-driver/CHANGELOG.md index f9c03c4d70d2b..351d937c8f07f 100644 --- a/packages/cubejs-base-driver/CHANGELOG.md +++ b/packages/cubejs-base-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/base-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/base-driver diff --git a/packages/cubejs-base-driver/package.json b/packages/cubejs-base-driver/package.json index dda5cc756bcae..f9eae0553b85a 100644 --- a/packages/cubejs-base-driver/package.json +++ b/packages/cubejs-base-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/base-driver", "description": "Cube.js Base Driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -33,11 +33,11 @@ "@aws-sdk/s3-request-presigner": "^3.49.0", "@azure/identity": "^4.4.1", "@azure/storage-blob": "^12.9.0", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/shared": "1.7.28", "@google-cloud/storage": "^7.13.0" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "@types/jest": "^29", "@types/node": "^22", "jest": "^29", diff --git a/packages/cubejs-bigquery-driver/CHANGELOG.md b/packages/cubejs-bigquery-driver/CHANGELOG.md index 1b323d3e5bc6d..7babf1af08460 100644 --- a/packages/cubejs-bigquery-driver/CHANGELOG.md +++ b/packages/cubejs-bigquery-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/bigquery-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/bigquery-driver diff --git a/packages/cubejs-bigquery-driver/package.json b/packages/cubejs-bigquery-driver/package.json index be427c75837de..65a5e9f97cc50 100644 --- a/packages/cubejs-bigquery-driver/package.json +++ b/packages/cubejs-bigquery-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/bigquery-driver", "description": "Cube.js BigQuery database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -29,15 +29,15 @@ "main": "index.js", "types": "dist/src/index.d.ts", "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", "@cubejs-backend/dotenv": "^9.0.2", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/shared": "1.7.28", "@google-cloud/bigquery": "^7.7.0", "@google-cloud/storage": "^7.13.0", "ramda": "^0.27.2" }, "devDependencies": { - "@cubejs-backend/testing-shared": "1.7.27", + "@cubejs-backend/testing-shared": "1.7.28", "@types/big.js": "^6.2.2", "@types/dedent": "^0.7.0", "@types/jest": "^29", diff --git a/packages/cubejs-cli/CHANGELOG.md b/packages/cubejs-cli/CHANGELOG.md index 96a54fd9078a2..2e1a5e567a580 100644 --- a/packages/cubejs-cli/CHANGELOG.md +++ b/packages/cubejs-cli/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package cubejs-cli + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package cubejs-cli diff --git a/packages/cubejs-cli/package.json b/packages/cubejs-cli/package.json index 2cb5bfd135381..d27f6576158c6 100644 --- a/packages/cubejs-cli/package.json +++ b/packages/cubejs-cli/package.json @@ -2,7 +2,7 @@ "name": "cubejs-cli", "description": "Cube.js Command Line Interface", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -30,10 +30,10 @@ "LICENSE" ], "dependencies": { - "@cubejs-backend/cloud": "1.7.27", + "@cubejs-backend/cloud": "1.7.28", "@cubejs-backend/dotenv": "^9.0.2", - "@cubejs-backend/schema-compiler": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/schema-compiler": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "chalk": "^2.4.2", "cli-progress": "^3.10", "commander": "^2.19.0", @@ -50,8 +50,8 @@ "colors": "1.4.0" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", - "@cubejs-backend/server": "1.7.27", + "@cubejs-backend/linter": "1.7.28", + "@cubejs-backend/server": "1.7.28", "@oclif/command": "^1.8.0", "@types/cli-progress": "^3.8.0", "@types/cross-spawn": "^6.0.2", diff --git a/packages/cubejs-clickhouse-driver/CHANGELOG.md b/packages/cubejs-clickhouse-driver/CHANGELOG.md index 2d7838c010291..fb1aa319a78f2 100644 --- a/packages/cubejs-clickhouse-driver/CHANGELOG.md +++ b/packages/cubejs-clickhouse-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/clickhouse-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/clickhouse-driver diff --git a/packages/cubejs-clickhouse-driver/package.json b/packages/cubejs-clickhouse-driver/package.json index ccf2498ae8295..ab4f5da8dd6ae 100644 --- a/packages/cubejs-clickhouse-driver/package.json +++ b/packages/cubejs-clickhouse-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/clickhouse-driver", "description": "Cube.js ClickHouse database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -29,16 +29,16 @@ }, "dependencies": { "@clickhouse/client": "^1.12.0", - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "moment": "^2.24.0", "sqlstring": "^2.3.1", "uuid": "^11.1.1" }, "license": "Apache-2.0", "devDependencies": { - "@cubejs-backend/linter": "1.7.27", - "@cubejs-backend/testing-shared": "1.7.27", + "@cubejs-backend/linter": "1.7.28", + "@cubejs-backend/testing-shared": "1.7.28", "@types/jest": "^29", "jest": "^29", "typescript": "~5.2.2" diff --git a/packages/cubejs-client-core/CHANGELOG.md b/packages/cubejs-client-core/CHANGELOG.md index 44fb3ee8ef4e6..328b69dd3fb2f 100644 --- a/packages/cubejs-client-core/CHANGELOG.md +++ b/packages/cubejs-client-core/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-client/core + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-client/core diff --git a/packages/cubejs-client-core/package.json b/packages/cubejs-client-core/package.json index ba8a45356a865..1865c8a7dc5d2 100644 --- a/packages/cubejs-client-core/package.json +++ b/packages/cubejs-client-core/package.json @@ -1,6 +1,6 @@ { "name": "@cubejs-client/core", - "version": "1.7.27", + "version": "1.7.28", "engines": {}, "type": "module", "repository": { @@ -58,7 +58,7 @@ ], "license": "MIT", "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "@types/d3-format": "^3", "@types/d3-time-format": "^4", "@types/moment-range": "^4.0.0", diff --git a/packages/cubejs-client-dx/CHANGELOG.md b/packages/cubejs-client-dx/CHANGELOG.md index 30c531fc211ae..1067446ed856f 100644 --- a/packages/cubejs-client-dx/CHANGELOG.md +++ b/packages/cubejs-client-dx/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-client/dx + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-client/dx diff --git a/packages/cubejs-client-dx/package.json b/packages/cubejs-client-dx/package.json index 48f91965efb86..4a9017e66120e 100644 --- a/packages/cubejs-client-dx/package.json +++ b/packages/cubejs-client-dx/package.json @@ -1,6 +1,6 @@ { "name": "@cubejs-client/dx", - "version": "1.7.27", + "version": "1.7.28", "engines": {}, "repository": { "type": "git", diff --git a/packages/cubejs-client-ngx/CHANGELOG.md b/packages/cubejs-client-ngx/CHANGELOG.md index 32f829c6b4da6..3c8f41be681f7 100644 --- a/packages/cubejs-client-ngx/CHANGELOG.md +++ b/packages/cubejs-client-ngx/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-client/ngx + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-client/ngx diff --git a/packages/cubejs-client-ngx/package.json b/packages/cubejs-client-ngx/package.json index 136f9a492a703..21ac9a14d43c7 100644 --- a/packages/cubejs-client-ngx/package.json +++ b/packages/cubejs-client-ngx/package.json @@ -1,6 +1,6 @@ { "name": "@cubejs-client/ngx", - "version": "1.7.27", + "version": "1.7.28", "author": "Cube Dev, Inc.", "engines": {}, "repository": { diff --git a/packages/cubejs-client-react/CHANGELOG.md b/packages/cubejs-client-react/CHANGELOG.md index d27f87eab87d9..665061d2f0f91 100644 --- a/packages/cubejs-client-react/CHANGELOG.md +++ b/packages/cubejs-client-react/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-client/react + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-client/react diff --git a/packages/cubejs-client-react/package.json b/packages/cubejs-client-react/package.json index 2e04b912c69ab..f266d1ae12ffe 100644 --- a/packages/cubejs-client-react/package.json +++ b/packages/cubejs-client-react/package.json @@ -1,6 +1,6 @@ { "name": "@cubejs-client/react", - "version": "1.7.27", + "version": "1.7.28", "author": "Cube Dev, Inc.", "license": "MIT", "engines": {}, @@ -26,7 +26,7 @@ ], "dependencies": { "@babel/runtime": "^7.1.2", - "@cubejs-client/core": "1.7.27", + "@cubejs-client/core": "1.7.28", "core-js": "^3.6.5", "ramda": "^0.27.2" }, diff --git a/packages/cubejs-client-vue3/CHANGELOG.md b/packages/cubejs-client-vue3/CHANGELOG.md index 8bf7974e71c84..36375c32cf04f 100644 --- a/packages/cubejs-client-vue3/CHANGELOG.md +++ b/packages/cubejs-client-vue3/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-client/vue3 + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-client/vue3 diff --git a/packages/cubejs-client-vue3/package.json b/packages/cubejs-client-vue3/package.json index f5e96384ed8fc..bd4bc226b7454 100644 --- a/packages/cubejs-client-vue3/package.json +++ b/packages/cubejs-client-vue3/package.json @@ -1,6 +1,6 @@ { "name": "@cubejs-client/vue3", - "version": "1.7.27", + "version": "1.7.28", "engines": {}, "repository": { "type": "git", @@ -27,7 +27,7 @@ "src" ], "dependencies": { - "@cubejs-client/core": "1.7.27", + "@cubejs-client/core": "1.7.28", "ramda": "^0.27.0" }, "devDependencies": { diff --git a/packages/cubejs-client-ws-transport/CHANGELOG.md b/packages/cubejs-client-ws-transport/CHANGELOG.md index 2fe08dcaa30ef..2f971fcb9096c 100644 --- a/packages/cubejs-client-ws-transport/CHANGELOG.md +++ b/packages/cubejs-client-ws-transport/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-client/ws-transport + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-client/ws-transport diff --git a/packages/cubejs-client-ws-transport/package.json b/packages/cubejs-client-ws-transport/package.json index b01f2d565e935..c9494f0ea742d 100644 --- a/packages/cubejs-client-ws-transport/package.json +++ b/packages/cubejs-client-ws-transport/package.json @@ -1,6 +1,6 @@ { "name": "@cubejs-client/ws-transport", - "version": "1.7.27", + "version": "1.7.28", "engines": {}, "repository": { "type": "git", @@ -20,7 +20,7 @@ }, "dependencies": { "@babel/runtime": "^7.1.2", - "@cubejs-client/core": "1.7.27", + "@cubejs-client/core": "1.7.28", "core-js": "^3.6.5", "isomorphic-ws": "^4.0.1", "ws": "^7.3.1" @@ -33,7 +33,7 @@ "@babel/core": "^7.3.3", "@babel/preset-env": "^7.3.1", "@babel/preset-typescript": "^7.12.1", - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "@types/ws": "^7.2.9", "typescript": "~5.2.2" }, diff --git a/packages/cubejs-crate-driver/CHANGELOG.md b/packages/cubejs-crate-driver/CHANGELOG.md index 6c95a6785b5fc..5e11febd61d87 100644 --- a/packages/cubejs-crate-driver/CHANGELOG.md +++ b/packages/cubejs-crate-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/crate-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/crate-driver diff --git a/packages/cubejs-crate-driver/package.json b/packages/cubejs-crate-driver/package.json index daed222cc526c..1daaeb392c711 100644 --- a/packages/cubejs-crate-driver/package.json +++ b/packages/cubejs-crate-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/crate-driver", "description": "Cube.js Crate database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -29,13 +29,13 @@ "lint:fix": "eslint --fix src/* --ext .ts" }, "dependencies": { - "@cubejs-backend/postgres-driver": "1.7.27", - "@cubejs-backend/shared": "1.7.27" + "@cubejs-backend/postgres-driver": "1.7.28", + "@cubejs-backend/shared": "1.7.28" }, "license": "Apache-2.0", "devDependencies": { - "@cubejs-backend/linter": "1.7.27", - "@cubejs-backend/testing-shared": "1.7.27", + "@cubejs-backend/linter": "1.7.28", + "@cubejs-backend/testing-shared": "1.7.28", "testcontainers": "^10.28.0", "typescript": "~5.2.2" }, diff --git a/packages/cubejs-cubestore-driver/CHANGELOG.md b/packages/cubejs-cubestore-driver/CHANGELOG.md index 62476e9e521ab..e0dab773a26e3 100644 --- a/packages/cubejs-cubestore-driver/CHANGELOG.md +++ b/packages/cubejs-cubestore-driver/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +### Bug Fixes + +- **cubestore-driver:** don't fail queries on `write EPIPE`, report over-limit messages readably ([#11490](https://github.com/cube-js/cube/issues/11490)) ([d9ff741](https://github.com/cube-js/cube/commit/d9ff741b991ae9fb0bde5b742f6d514f3a7f94d7)), closes [#11655](https://github.com/cube-js/cube/issues/11655) + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/cubestore-driver diff --git a/packages/cubejs-cubestore-driver/package.json b/packages/cubejs-cubestore-driver/package.json index aeba408acd1ee..6c06ae3a9cbb5 100644 --- a/packages/cubejs-cubestore-driver/package.json +++ b/packages/cubejs-cubestore-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/cubestore-driver", "description": "Cube Store driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -27,10 +27,10 @@ "unit": "jest --coverage" }, "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/cubestore": "1.7.27", - "@cubejs-backend/native": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/cubestore": "1.7.28", + "@cubejs-backend/native": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "csv-write-stream": "^2.0.0", "flatbuffers": "25.9.23", "fs-extra": "^9.1.0", @@ -41,7 +41,7 @@ "ws": "^7.4.3" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "@types/csv-write-stream": "^2.0.0", "@types/jest": "^29", "@types/node": "^22", diff --git a/packages/cubejs-databricks-jdbc-driver/CHANGELOG.md b/packages/cubejs-databricks-jdbc-driver/CHANGELOG.md index 053ab6ffbab45..49e46739ddcc1 100644 --- a/packages/cubejs-databricks-jdbc-driver/CHANGELOG.md +++ b/packages/cubejs-databricks-jdbc-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/databricks-jdbc-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/databricks-jdbc-driver diff --git a/packages/cubejs-databricks-jdbc-driver/package.json b/packages/cubejs-databricks-jdbc-driver/package.json index 43ff6bcbc63f1..2922707950ae2 100644 --- a/packages/cubejs-databricks-jdbc-driver/package.json +++ b/packages/cubejs-databricks-jdbc-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/databricks-jdbc-driver", "description": "Cube.js Databricks database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "license": "Apache-2.0", "repository": { "type": "git", @@ -30,17 +30,17 @@ "bin" ], "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/jdbc-driver": "1.7.27", - "@cubejs-backend/schema-compiler": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/jdbc-driver": "1.7.28", + "@cubejs-backend/schema-compiler": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "node-fetch": "^2.6.1", "ramda": "^0.27.2", "source-map-support": "^0.5.19", "uuid": "^11.1.1" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "@types/jest": "^29", "@types/node": "^22", "@types/ramda": "^0.27.34", diff --git a/packages/cubejs-dbt-schema-extension/CHANGELOG.md b/packages/cubejs-dbt-schema-extension/CHANGELOG.md index 65d77e2bbc650..6498bcdaf56e4 100644 --- a/packages/cubejs-dbt-schema-extension/CHANGELOG.md +++ b/packages/cubejs-dbt-schema-extension/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/dbt-schema-extension + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/dbt-schema-extension diff --git a/packages/cubejs-dbt-schema-extension/package.json b/packages/cubejs-dbt-schema-extension/package.json index f4dd77b87abf9..c7b5570c335d5 100644 --- a/packages/cubejs-dbt-schema-extension/package.json +++ b/packages/cubejs-dbt-schema-extension/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/dbt-schema-extension", "description": "Cube.js dbt Schema Extension", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -25,14 +25,14 @@ "lint:fix": "eslint --fix src/* --ext .ts,.js" }, "dependencies": { - "@cubejs-backend/schema-compiler": "1.7.27", + "@cubejs-backend/schema-compiler": "1.7.28", "fs-extra": "^9.1.0", "inflection": "^1.12.0", "node-fetch": "^2.6.1" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", - "@cubejs-backend/testing": "1.7.27", + "@cubejs-backend/linter": "1.7.28", + "@cubejs-backend/testing": "1.7.28", "@types/jest": "^29", "jest": "^29", "stream-to-array": "^2.3.0", diff --git a/packages/cubejs-docker/CHANGELOG.md b/packages/cubejs-docker/CHANGELOG.md index 4f71ca392e28f..dd686784a926c 100644 --- a/packages/cubejs-docker/CHANGELOG.md +++ b/packages/cubejs-docker/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/docker + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/docker diff --git a/packages/cubejs-docker/package.json b/packages/cubejs-docker/package.json index 72523ff40b5ea..f91df7753bfb6 100644 --- a/packages/cubejs-docker/package.json +++ b/packages/cubejs-docker/package.json @@ -1,6 +1,6 @@ { "name": "@cubejs-backend/docker", - "version": "1.7.27", + "version": "1.7.28", "description": "Cube.js In Docker (virtual package)", "author": "Cube Dev, Inc.", "license": "Apache-2.0", @@ -9,34 +9,34 @@ "node": ">=20.0.0" }, "dependencies": { - "@cubejs-backend/athena-driver": "1.7.27", - "@cubejs-backend/bigquery-driver": "1.7.27", - "@cubejs-backend/clickhouse-driver": "1.7.27", - "@cubejs-backend/crate-driver": "1.7.27", - "@cubejs-backend/databricks-jdbc-driver": "1.7.27", - "@cubejs-backend/dbt-schema-extension": "1.7.27", - "@cubejs-backend/dremio-driver": "1.7.27", - "@cubejs-backend/druid-driver": "1.7.27", - "@cubejs-backend/duckdb-driver": "1.7.27", - "@cubejs-backend/firebolt-driver": "1.7.27", - "@cubejs-backend/hive-driver": "1.7.27", - "@cubejs-backend/ksql-driver": "1.7.27", - "@cubejs-backend/materialize-driver": "1.7.27", - "@cubejs-backend/mongobi-driver": "1.7.27", - "@cubejs-backend/mssql-driver": "1.7.27", - "@cubejs-backend/mysql-driver": "1.7.27", - "@cubejs-backend/oracle-driver": "1.7.27", - "@cubejs-backend/pinot-driver": "1.7.27", - "@cubejs-backend/postgres-driver": "1.7.27", - "@cubejs-backend/prestodb-driver": "1.7.27", - "@cubejs-backend/questdb-driver": "1.7.27", - "@cubejs-backend/redshift-driver": "1.7.27", - "@cubejs-backend/server": "1.7.27", - "@cubejs-backend/snowflake-driver": "1.7.27", - "@cubejs-backend/sqlite-driver": "1.7.27", - "@cubejs-backend/trino-driver": "1.7.27", - "@cubejs-backend/vertica-driver": "1.7.27", - "cubejs-cli": "1.7.27", + "@cubejs-backend/athena-driver": "1.7.28", + "@cubejs-backend/bigquery-driver": "1.7.28", + "@cubejs-backend/clickhouse-driver": "1.7.28", + "@cubejs-backend/crate-driver": "1.7.28", + "@cubejs-backend/databricks-jdbc-driver": "1.7.28", + "@cubejs-backend/dbt-schema-extension": "1.7.28", + "@cubejs-backend/dremio-driver": "1.7.28", + "@cubejs-backend/druid-driver": "1.7.28", + "@cubejs-backend/duckdb-driver": "1.7.28", + "@cubejs-backend/firebolt-driver": "1.7.28", + "@cubejs-backend/hive-driver": "1.7.28", + "@cubejs-backend/ksql-driver": "1.7.28", + "@cubejs-backend/materialize-driver": "1.7.28", + "@cubejs-backend/mongobi-driver": "1.7.28", + "@cubejs-backend/mssql-driver": "1.7.28", + "@cubejs-backend/mysql-driver": "1.7.28", + "@cubejs-backend/oracle-driver": "1.7.28", + "@cubejs-backend/pinot-driver": "1.7.28", + "@cubejs-backend/postgres-driver": "1.7.28", + "@cubejs-backend/prestodb-driver": "1.7.28", + "@cubejs-backend/questdb-driver": "1.7.28", + "@cubejs-backend/redshift-driver": "1.7.28", + "@cubejs-backend/server": "1.7.28", + "@cubejs-backend/snowflake-driver": "1.7.28", + "@cubejs-backend/sqlite-driver": "1.7.28", + "@cubejs-backend/trino-driver": "1.7.28", + "@cubejs-backend/vertica-driver": "1.7.28", + "cubejs-cli": "1.7.28", "typescript": "~5.2.2" }, "resolutions": { diff --git a/packages/cubejs-dremio-driver/CHANGELOG.md b/packages/cubejs-dremio-driver/CHANGELOG.md index c48a43e5d42c5..a059d13f61e03 100644 --- a/packages/cubejs-dremio-driver/CHANGELOG.md +++ b/packages/cubejs-dremio-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/dremio-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/dremio-driver diff --git a/packages/cubejs-dremio-driver/package.json b/packages/cubejs-dremio-driver/package.json index 531adf3f7cb76..ee512d1cf0323 100644 --- a/packages/cubejs-dremio-driver/package.json +++ b/packages/cubejs-dremio-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/dremio-driver", "description": "Cube.js Dremio driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -23,14 +23,14 @@ "lint:fix": "eslint driver/*.js" }, "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/schema-compiler": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/schema-compiler": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "axios": "^1.8.3" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", - "@cubejs-backend/testing-shared": "1.7.27", + "@cubejs-backend/linter": "1.7.28", + "@cubejs-backend/testing-shared": "1.7.28", "jest": "^29" }, "license": "Apache-2.0", diff --git a/packages/cubejs-druid-driver/CHANGELOG.md b/packages/cubejs-druid-driver/CHANGELOG.md index bcd80e2de0def..6801d2383e144 100644 --- a/packages/cubejs-druid-driver/CHANGELOG.md +++ b/packages/cubejs-druid-driver/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +### Features + +- **cubesql:** Push UNION down to the data source ([#11651](https://github.com/cube-js/cube/issues/11651)) ([2aba3ac](https://github.com/cube-js/cube/commit/2aba3ac6d15b3f6885df952f3b0380dbbbd5a31d)) + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/druid-driver diff --git a/packages/cubejs-druid-driver/package.json b/packages/cubejs-druid-driver/package.json index 3026bf2187d91..9117e25e3847a 100644 --- a/packages/cubejs-druid-driver/package.json +++ b/packages/cubejs-druid-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/druid-driver", "description": "Cube.js Druid database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "license": "Apache-2.0", "repository": { "type": "git", @@ -28,13 +28,13 @@ "dist/src/*" ], "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/schema-compiler": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/schema-compiler": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "axios": "^1.8.3" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "@types/jest": "^29", "@types/node": "^22", "jest": "^29", diff --git a/packages/cubejs-duckdb-driver/CHANGELOG.md b/packages/cubejs-duckdb-driver/CHANGELOG.md index abe2bad845565..21e7f6bcb754a 100644 --- a/packages/cubejs-duckdb-driver/CHANGELOG.md +++ b/packages/cubejs-duckdb-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/duckdb-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/duckdb-driver diff --git a/packages/cubejs-duckdb-driver/package.json b/packages/cubejs-duckdb-driver/package.json index e4a578538d120..023c6fca31241 100644 --- a/packages/cubejs-duckdb-driver/package.json +++ b/packages/cubejs-duckdb-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/duckdb-driver", "description": "Cube DuckDB database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -28,15 +28,15 @@ "lint:fix": "eslint --fix src/* --ext .ts" }, "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/schema-compiler": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/schema-compiler": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "duckdb": "^1.4.1" }, "license": "Apache-2.0", "devDependencies": { - "@cubejs-backend/linter": "1.7.27", - "@cubejs-backend/testing-shared": "1.7.27", + "@cubejs-backend/linter": "1.7.28", + "@cubejs-backend/testing-shared": "1.7.28", "@types/jest": "^29", "@types/node": "^22", "jest": "^29", diff --git a/packages/cubejs-firebolt-driver/CHANGELOG.md b/packages/cubejs-firebolt-driver/CHANGELOG.md index 0f06bd44d9f04..6d5e5bcca144a 100644 --- a/packages/cubejs-firebolt-driver/CHANGELOG.md +++ b/packages/cubejs-firebolt-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/firebolt-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/firebolt-driver diff --git a/packages/cubejs-firebolt-driver/package.json b/packages/cubejs-firebolt-driver/package.json index f6be80adc7467..3edb279d6ce72 100644 --- a/packages/cubejs-firebolt-driver/package.json +++ b/packages/cubejs-firebolt-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/firebolt-driver", "description": "Cube.js Firebolt database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -28,15 +28,15 @@ "lint:fix": "eslint --fix src/* --ext .ts" }, "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/schema-compiler": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/schema-compiler": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "firebolt-sdk": "1.10.0" }, "license": "Apache-2.0", "devDependencies": { - "@cubejs-backend/linter": "1.7.27", - "@cubejs-backend/testing-shared": "1.7.27", + "@cubejs-backend/linter": "1.7.28", + "@cubejs-backend/testing-shared": "1.7.28", "typescript": "~5.2.2" }, "publishConfig": { diff --git a/packages/cubejs-hive-driver/CHANGELOG.md b/packages/cubejs-hive-driver/CHANGELOG.md index db347e65440e9..113b054d3599c 100644 --- a/packages/cubejs-hive-driver/CHANGELOG.md +++ b/packages/cubejs-hive-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/hive-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/hive-driver diff --git a/packages/cubejs-hive-driver/package.json b/packages/cubejs-hive-driver/package.json index 002911ad2f84f..fd22801d8d870 100644 --- a/packages/cubejs-hive-driver/package.json +++ b/packages/cubejs-hive-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/hive-driver", "description": "Cube.js Hive database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -17,8 +17,8 @@ "lint:fix": "eslint --fix src/* --ext .ts" }, "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "jshs2": "^0.4.4", "sasl-plain": "^0.1.0", "saslmechanisms": "^0.1.1", @@ -27,7 +27,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@cubejs-backend/linter": "1.7.27" + "@cubejs-backend/linter": "1.7.28" }, "publishConfig": { "access": "public" diff --git a/packages/cubejs-jdbc-driver/CHANGELOG.md b/packages/cubejs-jdbc-driver/CHANGELOG.md index 42fbda5675168..2ac4c9f5ff3dd 100644 --- a/packages/cubejs-jdbc-driver/CHANGELOG.md +++ b/packages/cubejs-jdbc-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/jdbc-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/jdbc-driver diff --git a/packages/cubejs-jdbc-driver/package.json b/packages/cubejs-jdbc-driver/package.json index 90e1f7eff4769..1e65bbe900350 100644 --- a/packages/cubejs-jdbc-driver/package.json +++ b/packages/cubejs-jdbc-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/jdbc-driver", "description": "Cube.js JDBC database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -26,9 +26,9 @@ "index.js" ], "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", "@cubejs-backend/node-java-maven": "^0.1.3", - "@cubejs-backend/shared": "1.7.27" + "@cubejs-backend/shared": "1.7.28" }, "optionalDependencies": { "@cubejs-backend/jdbc": "^0.9.0", @@ -42,7 +42,7 @@ "testEnvironment": "node" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "@types/node": "^22", "typescript": "~5.2.2" } diff --git a/packages/cubejs-ksql-driver/CHANGELOG.md b/packages/cubejs-ksql-driver/CHANGELOG.md index df0cae67705d3..eb0a5314cd698 100644 --- a/packages/cubejs-ksql-driver/CHANGELOG.md +++ b/packages/cubejs-ksql-driver/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +### Features + +- **cubesql:** Push UNION down to the data source ([#11651](https://github.com/cube-js/cube/issues/11651)) ([2aba3ac](https://github.com/cube-js/cube/commit/2aba3ac6d15b3f6885df952f3b0380dbbbd5a31d)) + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/ksql-driver diff --git a/packages/cubejs-ksql-driver/package.json b/packages/cubejs-ksql-driver/package.json index 890bebc780389..9808f03761b60 100644 --- a/packages/cubejs-ksql-driver/package.json +++ b/packages/cubejs-ksql-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/ksql-driver", "description": "Cube.js ksql database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -26,9 +26,9 @@ "lint:fix": "eslint --fix src/* --ext .ts" }, "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/schema-compiler": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/schema-compiler": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "async-mutex": "0.3.2", "axios": "^1.8.3", "kafkajs": "^2.2.3" @@ -41,7 +41,7 @@ "extends": "../cubejs-linter" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "typescript": "~5.2.2" } } diff --git a/packages/cubejs-linter/CHANGELOG.md b/packages/cubejs-linter/CHANGELOG.md index 3282e36658616..49d676961bfd7 100644 --- a/packages/cubejs-linter/CHANGELOG.md +++ b/packages/cubejs-linter/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/linter + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/linter diff --git a/packages/cubejs-linter/package.json b/packages/cubejs-linter/package.json index bcf436e7c1a9d..6325b24d02c89 100644 --- a/packages/cubejs-linter/package.json +++ b/packages/cubejs-linter/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/linter", "description": "Cube.js ESLint (virtual package) for linting code", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", diff --git a/packages/cubejs-materialize-driver/CHANGELOG.md b/packages/cubejs-materialize-driver/CHANGELOG.md index 08ba06d695699..09dfa15906788 100644 --- a/packages/cubejs-materialize-driver/CHANGELOG.md +++ b/packages/cubejs-materialize-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/materialize-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/materialize-driver diff --git a/packages/cubejs-materialize-driver/package.json b/packages/cubejs-materialize-driver/package.json index f6d8a5a1f6cb6..a35bfd95fd763 100644 --- a/packages/cubejs-materialize-driver/package.json +++ b/packages/cubejs-materialize-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/materialize-driver", "description": "Cube.js Materialize database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -27,15 +27,15 @@ "lint:fix": "eslint --fix src/* --ext .ts" }, "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/postgres-driver": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/postgres-driver": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "semver": "^7.6.3" }, "license": "Apache-2.0", "devDependencies": { - "@cubejs-backend/linter": "1.7.27", - "@cubejs-backend/testing": "1.7.27", + "@cubejs-backend/linter": "1.7.28", + "@cubejs-backend/testing": "1.7.28", "typescript": "~5.2.2" }, "publishConfig": { diff --git a/packages/cubejs-mongobi-driver/CHANGELOG.md b/packages/cubejs-mongobi-driver/CHANGELOG.md index 1cfb8a0c2905b..36ea9dbef5570 100644 --- a/packages/cubejs-mongobi-driver/CHANGELOG.md +++ b/packages/cubejs-mongobi-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/mongobi-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/mongobi-driver diff --git a/packages/cubejs-mongobi-driver/package.json b/packages/cubejs-mongobi-driver/package.json index 1531c40ef1a43..deec376e37ebd 100644 --- a/packages/cubejs-mongobi-driver/package.json +++ b/packages/cubejs-mongobi-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/mongobi-driver", "description": "Cube.js MongoBI driver", "author": "krunalsabnis@gmail.com", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -27,8 +27,8 @@ "integration:mongobi": "jest dist/test" }, "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "@types/node": "^22", "moment": "^2.29.1", "mysql2": "^3.11.5" @@ -38,7 +38,7 @@ "access": "public" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "testcontainers": "^10.28.0", "typescript": "~5.2.2" }, diff --git a/packages/cubejs-mssql-driver/CHANGELOG.md b/packages/cubejs-mssql-driver/CHANGELOG.md index a1ed20066cad3..d13c7056c13a2 100644 --- a/packages/cubejs-mssql-driver/CHANGELOG.md +++ b/packages/cubejs-mssql-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/mssql-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/mssql-driver diff --git a/packages/cubejs-mssql-driver/package.json b/packages/cubejs-mssql-driver/package.json index bca390144ac1e..ac18b8d7c6a5e 100644 --- a/packages/cubejs-mssql-driver/package.json +++ b/packages/cubejs-mssql-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/mssql-driver", "description": "Cube.js MS SQL database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -25,8 +25,8 @@ "lint:fix": "eslint --fix src/* --ext .ts,.js" }, "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "mssql": "^11.0.1" }, "devDependencies": { diff --git a/packages/cubejs-mysql-aurora-serverless-driver/CHANGELOG.md b/packages/cubejs-mysql-aurora-serverless-driver/CHANGELOG.md index 71d3e0542ebe8..bac945f8ce0b3 100644 --- a/packages/cubejs-mysql-aurora-serverless-driver/CHANGELOG.md +++ b/packages/cubejs-mysql-aurora-serverless-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/mysql-aurora-serverless-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/mysql-aurora-serverless-driver diff --git a/packages/cubejs-mysql-aurora-serverless-driver/package.json b/packages/cubejs-mysql-aurora-serverless-driver/package.json index 751f7984415c4..ad32f02783745 100644 --- a/packages/cubejs-mysql-aurora-serverless-driver/package.json +++ b/packages/cubejs-mysql-aurora-serverless-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/mysql-aurora-serverless-driver", "description": "Cube.js Aurora Serverless Mysql database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -21,14 +21,14 @@ "lint": "eslint driver/*.js test/*.js" }, "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "@types/mysql": "^2.15.15", "aws-sdk": "^2.787.0", "data-api-client": "^1.1.0" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "@types/data-api-client": "^1.2.1", "@types/jest": "^29", "jest": "^29", diff --git a/packages/cubejs-mysql-driver/CHANGELOG.md b/packages/cubejs-mysql-driver/CHANGELOG.md index 9764335438f1f..07b42b5b95027 100644 --- a/packages/cubejs-mysql-driver/CHANGELOG.md +++ b/packages/cubejs-mysql-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/mysql-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/mysql-driver diff --git a/packages/cubejs-mysql-driver/package.json b/packages/cubejs-mysql-driver/package.json index 8fc97a6aed142..6c0712b1710c4 100644 --- a/packages/cubejs-mysql-driver/package.json +++ b/packages/cubejs-mysql-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/mysql-driver", "description": "Cube.js Mysql database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -27,13 +27,13 @@ "lint:fix": "eslint --fix src/* test/* --ext .ts,.js" }, "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "mysql2": "^3.16.1" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", - "@cubejs-backend/testing-shared": "1.7.27", + "@cubejs-backend/linter": "1.7.28", + "@cubejs-backend/testing-shared": "1.7.28", "@types/jest": "^29", "jest": "^29", "stream-to-array": "^2.3.0", diff --git a/packages/cubejs-oracle-driver/CHANGELOG.md b/packages/cubejs-oracle-driver/CHANGELOG.md index 54ffc9154e3ec..1e600ebc94a1b 100644 --- a/packages/cubejs-oracle-driver/CHANGELOG.md +++ b/packages/cubejs-oracle-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/oracle-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/oracle-driver diff --git a/packages/cubejs-oracle-driver/package.json b/packages/cubejs-oracle-driver/package.json index ba7b718b2cca4..680ac47c6de65 100644 --- a/packages/cubejs-oracle-driver/package.json +++ b/packages/cubejs-oracle-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/oracle-driver", "description": "Cube.js oracle database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -13,8 +13,8 @@ }, "main": "driver/OracleDriver.js", "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "ramda": "^0.27.0" }, "devDependencies": { diff --git a/packages/cubejs-pinot-driver/CHANGELOG.md b/packages/cubejs-pinot-driver/CHANGELOG.md index 2d632f4e3f1c5..fdc893e539d8d 100644 --- a/packages/cubejs-pinot-driver/CHANGELOG.md +++ b/packages/cubejs-pinot-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/pinot-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/pinot-driver diff --git a/packages/cubejs-pinot-driver/package.json b/packages/cubejs-pinot-driver/package.json index f4b1ccacc9d3c..98e8deaa4b33d 100644 --- a/packages/cubejs-pinot-driver/package.json +++ b/packages/cubejs-pinot-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/pinot-driver", "description": "Cube.js Pinot database driver", "author": "Julian Ronsse, InTheMemory, Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -28,9 +28,9 @@ "lint:fix": "eslint --fix src/* --ext .ts" }, "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/schema-compiler": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/schema-compiler": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "node-fetch": "^2.6.1", "ramda": "^0.27.2" }, @@ -39,7 +39,7 @@ "access": "public" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "@types/jest": "^29", "jest": "^29", "should": "^13.2.3", diff --git a/packages/cubejs-playground/CHANGELOG.md b/packages/cubejs-playground/CHANGELOG.md index d7d581fec092d..7116424ca53f6 100644 --- a/packages/cubejs-playground/CHANGELOG.md +++ b/packages/cubejs-playground/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-client/playground + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-client/playground diff --git a/packages/cubejs-playground/package.json b/packages/cubejs-playground/package.json index db613bde80caf..9c87f3853a47b 100644 --- a/packages/cubejs-playground/package.json +++ b/packages/cubejs-playground/package.json @@ -1,7 +1,7 @@ { "name": "@cubejs-client/playground", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "engines": {}, "repository": { "type": "git", @@ -68,8 +68,8 @@ "@ant-design/compatible": "^1.0.1", "@ant-design/icons": "^5.3.5", "@cube-dev/ui-kit": "0.52.3", - "@cubejs-client/core": "1.7.27", - "@cubejs-client/react": "1.7.27", + "@cubejs-client/core": "1.7.28", + "@cubejs-client/react": "1.7.28", "@types/flexsearch": "^0.7.3", "@types/node": "^22", "@types/react": "^18.3.4", diff --git a/packages/cubejs-postgres-driver/CHANGELOG.md b/packages/cubejs-postgres-driver/CHANGELOG.md index ca607a388629d..196a07cb318ce 100644 --- a/packages/cubejs-postgres-driver/CHANGELOG.md +++ b/packages/cubejs-postgres-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/postgres-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/postgres-driver diff --git a/packages/cubejs-postgres-driver/package.json b/packages/cubejs-postgres-driver/package.json index 9dcc61a593613..57d341c6b6f21 100644 --- a/packages/cubejs-postgres-driver/package.json +++ b/packages/cubejs-postgres-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/postgres-driver", "description": "Cube.js Postgres database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -27,8 +27,8 @@ "lint:fix": "eslint --fix src/* --ext .ts" }, "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "@types/pg": "^8.16.0", "@types/pg-query-stream": "^1.0.3", "pg": "^8.18.0", @@ -36,8 +36,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@cubejs-backend/linter": "1.7.27", - "@cubejs-backend/testing-shared": "1.7.27", + "@cubejs-backend/linter": "1.7.28", + "@cubejs-backend/testing-shared": "1.7.28", "testcontainers": "^10.28.0", "typescript": "~5.2.2" }, diff --git a/packages/cubejs-prestodb-driver/CHANGELOG.md b/packages/cubejs-prestodb-driver/CHANGELOG.md index 3b90a03a0eaf8..3e288fad3ad90 100644 --- a/packages/cubejs-prestodb-driver/CHANGELOG.md +++ b/packages/cubejs-prestodb-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/prestodb-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/prestodb-driver diff --git a/packages/cubejs-prestodb-driver/package.json b/packages/cubejs-prestodb-driver/package.json index 299d712441a16..e59b168c37d20 100644 --- a/packages/cubejs-prestodb-driver/package.json +++ b/packages/cubejs-prestodb-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/prestodb-driver", "description": "Cube.js Presto database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -28,8 +28,8 @@ "lint:fix": "eslint --fix src/* --ext .ts" }, "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "presto-client": "1.2.0", "ramda": "^0.27.0" }, @@ -38,7 +38,7 @@ "access": "public" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "@types/jest": "^29", "jest": "^29", "should": "^13.2.3", diff --git a/packages/cubejs-query-orchestrator/CHANGELOG.md b/packages/cubejs-query-orchestrator/CHANGELOG.md index 323fa211865be..d1a8c51c62e2a 100644 --- a/packages/cubejs-query-orchestrator/CHANGELOG.md +++ b/packages/cubejs-query-orchestrator/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/query-orchestrator + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/query-orchestrator diff --git a/packages/cubejs-query-orchestrator/package.json b/packages/cubejs-query-orchestrator/package.json index 8c5dd1d79ef19..9d1c663d82f18 100644 --- a/packages/cubejs-query-orchestrator/package.json +++ b/packages/cubejs-query-orchestrator/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/query-orchestrator", "description": "Cube.js Query Orchestrator and Cache", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -30,15 +30,15 @@ "dist/src/*" ], "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/cubestore-driver": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/cubestore-driver": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "csv-write-stream": "^2.0.0", "lru-cache": "^11.1.0", "ramda": "^0.27.2" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "@types/jest": "^29", "@types/node": "^22", "@types/ramda": "^0.27.32", diff --git a/packages/cubejs-questdb-driver/CHANGELOG.md b/packages/cubejs-questdb-driver/CHANGELOG.md index a9a0a210771ca..e80040a05a7dc 100644 --- a/packages/cubejs-questdb-driver/CHANGELOG.md +++ b/packages/cubejs-questdb-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/questdb-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/questdb-driver diff --git a/packages/cubejs-questdb-driver/package.json b/packages/cubejs-questdb-driver/package.json index 803b3e0c1212c..8fd04fda1a6c9 100644 --- a/packages/cubejs-questdb-driver/package.json +++ b/packages/cubejs-questdb-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/questdb-driver", "description": "Cube.js QuestDB database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -27,9 +27,9 @@ "lint:fix": "eslint --fix src/* --ext .ts" }, "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/schema-compiler": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/schema-compiler": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "@types/pg": "^8.6.0", "moment": "^2.24.0", "pg": "^8.7.0", @@ -37,8 +37,8 @@ }, "license": "Apache-2.0", "devDependencies": { - "@cubejs-backend/linter": "1.7.27", - "@cubejs-backend/testing-shared": "1.7.27", + "@cubejs-backend/linter": "1.7.28", + "@cubejs-backend/testing-shared": "1.7.28", "testcontainers": "^10.28.0", "typescript": "~5.2.2" }, diff --git a/packages/cubejs-redshift-driver/CHANGELOG.md b/packages/cubejs-redshift-driver/CHANGELOG.md index b70f383023689..bdd37a47b0bb2 100644 --- a/packages/cubejs-redshift-driver/CHANGELOG.md +++ b/packages/cubejs-redshift-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/redshift-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/redshift-driver diff --git a/packages/cubejs-redshift-driver/package.json b/packages/cubejs-redshift-driver/package.json index 2d3c3468e3354..25a6ba696b00d 100644 --- a/packages/cubejs-redshift-driver/package.json +++ b/packages/cubejs-redshift-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/redshift-driver", "description": "Cube.js Redshift database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -27,13 +27,13 @@ "dependencies": { "@aws-sdk/client-redshift": "^3.22.0", "@aws-sdk/credential-providers": "^3.22.0", - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/postgres-driver": "1.7.27", - "@cubejs-backend/shared": "1.7.27" + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/postgres-driver": "1.7.28", + "@cubejs-backend/shared": "1.7.28" }, "license": "Apache-2.0", "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "typescript": "~5.2.2" }, "publishConfig": { diff --git a/packages/cubejs-schema-compiler/CHANGELOG.md b/packages/cubejs-schema-compiler/CHANGELOG.md index 71ae6ab9fa685..4f6f2f2362022 100644 --- a/packages/cubejs-schema-compiler/CHANGELOG.md +++ b/packages/cubejs-schema-compiler/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +### Features + +- **cubesql:** Push UNION down to the data source ([#11651](https://github.com/cube-js/cube/issues/11651)) ([2aba3ac](https://github.com/cube-js/cube/commit/2aba3ac6d15b3f6885df952f3b0380dbbbd5a31d)) + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) ### Bug Fixes diff --git a/packages/cubejs-schema-compiler/package.json b/packages/cubejs-schema-compiler/package.json index 3f7833bee51fd..7d3e68b78148e 100644 --- a/packages/cubejs-schema-compiler/package.json +++ b/packages/cubejs-schema-compiler/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/schema-compiler", "description": "Cube schema compiler", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -40,8 +40,8 @@ "@babel/standalone": "^7.24", "@babel/traverse": "^7.24", "@babel/types": "^7.24", - "@cubejs-backend/native": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/native": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "antlr4": "^4.13.2", "camelcase": "^6.2.0", "cron-parser": "^4.9.0", @@ -60,9 +60,9 @@ }, "devDependencies": { "@clickhouse/client": "^1.12.0", - "@cubejs-backend/linter": "1.7.27", - "@cubejs-backend/mssql-driver": "1.7.27", - "@cubejs-backend/query-orchestrator": "1.7.27", + "@cubejs-backend/linter": "1.7.28", + "@cubejs-backend/mssql-driver": "1.7.28", + "@cubejs-backend/query-orchestrator": "1.7.28", "@types/babel__code-frame": "^7.0.6", "@types/babel__generator": "^7.6.8", "@types/babel__traverse": "^7.20.5", diff --git a/packages/cubejs-server-core/CHANGELOG.md b/packages/cubejs-server-core/CHANGELOG.md index 7429bc10d2e73..ffed88e0060f2 100644 --- a/packages/cubejs-server-core/CHANGELOG.md +++ b/packages/cubejs-server-core/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/server-core + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/server-core diff --git a/packages/cubejs-server-core/package.json b/packages/cubejs-server-core/package.json index ac2cab1603083..e0e6fbca5a388 100644 --- a/packages/cubejs-server-core/package.json +++ b/packages/cubejs-server-core/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/server-core", "description": "Cube.js base component to wire all backend components together", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -29,16 +29,16 @@ "unit": "jest --runInBand --forceExit --coverage dist/test" }, "dependencies": { - "@cubejs-backend/api-gateway": "1.7.27", - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/cloud": "1.7.27", - "@cubejs-backend/cubestore-driver": "1.7.27", + "@cubejs-backend/api-gateway": "1.7.28", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/cloud": "1.7.28", + "@cubejs-backend/cubestore-driver": "1.7.28", "@cubejs-backend/dotenv": "^9.0.2", - "@cubejs-backend/native": "1.7.27", - "@cubejs-backend/query-orchestrator": "1.7.27", - "@cubejs-backend/schema-compiler": "1.7.27", - "@cubejs-backend/shared": "1.7.27", - "@cubejs-backend/templates": "1.7.27", + "@cubejs-backend/native": "1.7.28", + "@cubejs-backend/query-orchestrator": "1.7.28", + "@cubejs-backend/schema-compiler": "1.7.28", + "@cubejs-backend/shared": "1.7.28", + "@cubejs-backend/templates": "1.7.28", "codesandbox-import-utils": "^2.1.12", "cross-spawn": "^7.0.1", "fs-extra": "^8.1.0", @@ -62,8 +62,8 @@ "ws": "^7.5.3" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", - "@cubejs-client/playground": "1.7.27", + "@cubejs-backend/linter": "1.7.28", + "@cubejs-client/playground": "1.7.28", "@types/cross-spawn": "^6.0.2", "@types/express": "^4.17.21", "@types/fs-extra": "^9.0.8", diff --git a/packages/cubejs-server/CHANGELOG.md b/packages/cubejs-server/CHANGELOG.md index 3319b8bb6f468..5ed77caf533f5 100644 --- a/packages/cubejs-server/CHANGELOG.md +++ b/packages/cubejs-server/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/server + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/server diff --git a/packages/cubejs-server/package.json b/packages/cubejs-server/package.json index 24a63fbe17002..118cf4da9aaf8 100644 --- a/packages/cubejs-server/package.json +++ b/packages/cubejs-server/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/server", "description": "Cube.js all-in-one server", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "types": "index.d.ts", "repository": { "type": "git", @@ -40,11 +40,11 @@ "jest:shapshot": "jest --updateSnapshot test" }, "dependencies": { - "@cubejs-backend/cubestore-driver": "1.7.27", + "@cubejs-backend/cubestore-driver": "1.7.28", "@cubejs-backend/dotenv": "^9.0.2", - "@cubejs-backend/native": "1.7.27", - "@cubejs-backend/server-core": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/native": "1.7.28", + "@cubejs-backend/server-core": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "@oclif/color": "^1.0.0", "@oclif/command": "^1.8.13", "@oclif/config": "^1.18.2", @@ -61,8 +61,8 @@ "ws": "^7.1.2" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", - "@cubejs-backend/query-orchestrator": "1.7.27", + "@cubejs-backend/linter": "1.7.28", + "@cubejs-backend/query-orchestrator": "1.7.28", "@oclif/dev-cli": "^1.23.1", "@types/body-parser": "^1.19.0", "@types/cors": "^2.8.8", diff --git a/packages/cubejs-snowflake-driver/CHANGELOG.md b/packages/cubejs-snowflake-driver/CHANGELOG.md index 0e5425ea53886..52a016fa182a2 100644 --- a/packages/cubejs-snowflake-driver/CHANGELOG.md +++ b/packages/cubejs-snowflake-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/snowflake-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/snowflake-driver diff --git a/packages/cubejs-snowflake-driver/package.json b/packages/cubejs-snowflake-driver/package.json index 2565ea174ec9d..bb1a62e7d40d7 100644 --- a/packages/cubejs-snowflake-driver/package.json +++ b/packages/cubejs-snowflake-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/snowflake-driver", "description": "Cube.js Snowflake database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -29,8 +29,8 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.726.0", - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "snowflake-sdk": "^2.4.0" }, "license": "Apache-2.0", @@ -41,7 +41,7 @@ "extends": "../cubejs-linter" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "typescript": "~5.2.2", "vitest": "^4" } diff --git a/packages/cubejs-sqlite-driver/CHANGELOG.md b/packages/cubejs-sqlite-driver/CHANGELOG.md index 5f4de4cf71311..9d47520b90747 100644 --- a/packages/cubejs-sqlite-driver/CHANGELOG.md +++ b/packages/cubejs-sqlite-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/sqlite-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/sqlite-driver diff --git a/packages/cubejs-sqlite-driver/package.json b/packages/cubejs-sqlite-driver/package.json index 08d93e90f705e..56d90e0fb9588 100644 --- a/packages/cubejs-sqlite-driver/package.json +++ b/packages/cubejs-sqlite-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/sqlite-driver", "description": "Cube.js Sqlite database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -18,13 +18,13 @@ "unit": "jest" }, "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "sqlite3": "^5.1.7" }, "license": "Apache-2.0", "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "jest": "^29" }, "publishConfig": { diff --git a/packages/cubejs-templates/CHANGELOG.md b/packages/cubejs-templates/CHANGELOG.md index 2e30d5698b4b7..b6be3ec1726bd 100644 --- a/packages/cubejs-templates/CHANGELOG.md +++ b/packages/cubejs-templates/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/templates + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/templates diff --git a/packages/cubejs-templates/package.json b/packages/cubejs-templates/package.json index 3831eb914013f..489d65f155895 100644 --- a/packages/cubejs-templates/package.json +++ b/packages/cubejs-templates/package.json @@ -1,6 +1,6 @@ { "name": "@cubejs-backend/templates", - "version": "1.7.27", + "version": "1.7.28", "description": "Cube.js Templates helpers", "author": "Cube Dev, Inc.", "repository": { @@ -31,7 +31,7 @@ "extends": "../cubejs-linter" }, "dependencies": { - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/shared": "1.7.28", "cross-spawn": "^7.0.3", "fs-extra": "^9.1.0", "node-fetch": "^2.6.1", @@ -40,7 +40,7 @@ "tar": "^7.5.22" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "typescript": "~5.2.2" } } diff --git a/packages/cubejs-testing-drivers/CHANGELOG.md b/packages/cubejs-testing-drivers/CHANGELOG.md index 214e193110e57..0f4588f3e03a5 100644 --- a/packages/cubejs-testing-drivers/CHANGELOG.md +++ b/packages/cubejs-testing-drivers/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/testing-drivers + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/testing-drivers diff --git a/packages/cubejs-testing-drivers/package.json b/packages/cubejs-testing-drivers/package.json index c70c2b3513fe1..7fc12e08e3f20 100644 --- a/packages/cubejs-testing-drivers/package.json +++ b/packages/cubejs-testing-drivers/package.json @@ -1,6 +1,6 @@ { "name": "@cubejs-backend/testing-drivers", - "version": "1.7.27", + "version": "1.7.28", "description": "Cube.js drivers test suite", "author": "Cube Dev, Inc.", "repository": { @@ -87,29 +87,29 @@ "dist/src" ], "dependencies": { - "@cubejs-backend/athena-driver": "1.7.27", - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/bigquery-driver": "1.7.27", - "@cubejs-backend/clickhouse-driver": "1.7.27", - "@cubejs-backend/crate-driver": "1.7.27", - "@cubejs-backend/cubestore-driver": "1.7.27", - "@cubejs-backend/databricks-jdbc-driver": "1.7.27", + "@cubejs-backend/athena-driver": "1.7.28", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/bigquery-driver": "1.7.28", + "@cubejs-backend/clickhouse-driver": "1.7.28", + "@cubejs-backend/crate-driver": "1.7.28", + "@cubejs-backend/cubestore-driver": "1.7.28", + "@cubejs-backend/databricks-jdbc-driver": "1.7.28", "@cubejs-backend/dotenv": "^9.0.2", - "@cubejs-backend/linter": "1.7.27", - "@cubejs-backend/mssql-driver": "1.7.27", - "@cubejs-backend/mysql-driver": "1.7.27", - "@cubejs-backend/oracle-driver": "1.7.27", - "@cubejs-backend/pinot-driver": "1.7.27", - "@cubejs-backend/postgres-driver": "1.7.27", - "@cubejs-backend/query-orchestrator": "1.7.27", - "@cubejs-backend/questdb-driver": "1.7.27", - "@cubejs-backend/server-core": "1.7.27", - "@cubejs-backend/shared": "1.7.27", - "@cubejs-backend/snowflake-driver": "1.7.27", - "@cubejs-backend/testing-shared": "1.7.27", - "@cubejs-backend/trino-driver": "1.7.27", - "@cubejs-client/core": "1.7.27", - "@cubejs-client/ws-transport": "1.7.27", + "@cubejs-backend/linter": "1.7.28", + "@cubejs-backend/mssql-driver": "1.7.28", + "@cubejs-backend/mysql-driver": "1.7.28", + "@cubejs-backend/oracle-driver": "1.7.28", + "@cubejs-backend/pinot-driver": "1.7.28", + "@cubejs-backend/postgres-driver": "1.7.28", + "@cubejs-backend/query-orchestrator": "1.7.28", + "@cubejs-backend/questdb-driver": "1.7.28", + "@cubejs-backend/server-core": "1.7.28", + "@cubejs-backend/shared": "1.7.28", + "@cubejs-backend/snowflake-driver": "1.7.28", + "@cubejs-backend/testing-shared": "1.7.28", + "@cubejs-backend/trino-driver": "1.7.28", + "@cubejs-client/core": "1.7.28", + "@cubejs-client/ws-transport": "1.7.28", "@jest/globals": "^29", "@types/jest": "^29", "@types/node": "^22", diff --git a/packages/cubejs-testing-shared/CHANGELOG.md b/packages/cubejs-testing-shared/CHANGELOG.md index 715c417e56823..1e2c0946dba4c 100644 --- a/packages/cubejs-testing-shared/CHANGELOG.md +++ b/packages/cubejs-testing-shared/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/testing-shared + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/testing-shared diff --git a/packages/cubejs-testing-shared/package.json b/packages/cubejs-testing-shared/package.json index 3ea08e3f76190..0332a59ef0e21 100644 --- a/packages/cubejs-testing-shared/package.json +++ b/packages/cubejs-testing-shared/package.json @@ -1,6 +1,6 @@ { "name": "@cubejs-backend/testing-shared", - "version": "1.7.27", + "version": "1.7.28", "description": "Cube.js Testing Helpers", "author": "Cube Dev, Inc.", "repository": { @@ -26,16 +26,16 @@ ], "dependencies": { "@cubejs-backend/dotenv": "^9.0.2", - "@cubejs-backend/query-orchestrator": "1.7.27", - "@cubejs-backend/schema-compiler": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/query-orchestrator": "1.7.28", + "@cubejs-backend/schema-compiler": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "@testcontainers/kafka": "~10.28.0", "dedent": "^0.7.0", "node-fetch": "^2.6.7", "testcontainers": "^10.28.0" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "@jest/globals": "^29", "@types/dedent": "^0.7.0", "@types/jest": "^29", diff --git a/packages/cubejs-testing/CHANGELOG.md b/packages/cubejs-testing/CHANGELOG.md index 7060e51c7eb4f..03ae39ee3f006 100644 --- a/packages/cubejs-testing/CHANGELOG.md +++ b/packages/cubejs-testing/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/testing + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/testing diff --git a/packages/cubejs-testing/package.json b/packages/cubejs-testing/package.json index 7ebda6080aaab..c81a47eb572b0 100644 --- a/packages/cubejs-testing/package.json +++ b/packages/cubejs-testing/package.json @@ -1,6 +1,6 @@ { "name": "@cubejs-backend/testing", - "version": "1.7.27", + "version": "1.7.28", "description": "Cube.js e2e tests", "author": "Cube Dev, Inc.", "repository": { @@ -91,15 +91,15 @@ "birdbox-fixtures" ], "dependencies": { - "@cubejs-backend/cubestore-driver": "1.7.27", + "@cubejs-backend/cubestore-driver": "1.7.28", "@cubejs-backend/dotenv": "^9.0.2", - "@cubejs-backend/ksql-driver": "1.7.27", - "@cubejs-backend/postgres-driver": "1.7.27", - "@cubejs-backend/query-orchestrator": "1.7.27", - "@cubejs-backend/schema-compiler": "1.7.27", - "@cubejs-backend/shared": "1.7.27", - "@cubejs-backend/testing-shared": "1.7.27", - "@cubejs-client/ws-transport": "1.7.27", + "@cubejs-backend/ksql-driver": "1.7.28", + "@cubejs-backend/postgres-driver": "1.7.28", + "@cubejs-backend/query-orchestrator": "1.7.28", + "@cubejs-backend/schema-compiler": "1.7.28", + "@cubejs-backend/shared": "1.7.28", + "@cubejs-backend/testing-shared": "1.7.28", + "@cubejs-client/ws-transport": "1.7.28", "dedent": "^0.7.0", "fs-extra": "^8.1.0", "http-proxy": "^1.18.1", @@ -110,8 +110,8 @@ }, "devDependencies": { "@4tw/cypress-drag-drop": "^1.6.0", - "@cubejs-backend/linter": "1.7.27", - "@cubejs-client/core": "1.7.27", + "@cubejs-backend/linter": "1.7.28", + "@cubejs-client/core": "1.7.28", "@jest/globals": "^29", "@types/dedent": "^0.7.0", "@types/http-proxy": "^1.17.5", diff --git a/packages/cubejs-trino-driver/CHANGELOG.md b/packages/cubejs-trino-driver/CHANGELOG.md index 5825b2f0ef28a..75a49efbf1744 100644 --- a/packages/cubejs-trino-driver/CHANGELOG.md +++ b/packages/cubejs-trino-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/trino-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/trino-driver diff --git a/packages/cubejs-trino-driver/package.json b/packages/cubejs-trino-driver/package.json index 7947a673691dd..52574ce732d24 100644 --- a/packages/cubejs-trino-driver/package.json +++ b/packages/cubejs-trino-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/trino-driver", "description": "Cube.js Trino database driver", "author": "Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -28,10 +28,10 @@ "lint:fix": "eslint --fix src/* --ext .ts" }, "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/prestodb-driver": "1.7.27", - "@cubejs-backend/schema-compiler": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/prestodb-driver": "1.7.28", + "@cubejs-backend/schema-compiler": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "node-fetch": "^2.6.1", "presto-client": "^1.2.0" }, @@ -40,7 +40,7 @@ "access": "public" }, "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "@types/jest": "^29", "jest": "^29", "testcontainers": "^10.28.0", diff --git a/packages/cubejs-vertica-driver/CHANGELOG.md b/packages/cubejs-vertica-driver/CHANGELOG.md index 85f5b77ffbed2..7c1942b544787 100644 --- a/packages/cubejs-vertica-driver/CHANGELOG.md +++ b/packages/cubejs-vertica-driver/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +**Note:** Version bump only for package @cubejs-backend/vertica-driver + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) **Note:** Version bump only for package @cubejs-backend/vertica-driver diff --git a/packages/cubejs-vertica-driver/package.json b/packages/cubejs-vertica-driver/package.json index e40c95dcf142d..ae1dabf1ec283 100644 --- a/packages/cubejs-vertica-driver/package.json +++ b/packages/cubejs-vertica-driver/package.json @@ -2,7 +2,7 @@ "name": "@cubejs-backend/vertica-driver", "description": "Cube.js Vertica database driver", "author": "Eduard Karacharov, Tim Brown, Cube Dev, Inc.", - "version": "1.7.27", + "version": "1.7.28", "repository": { "type": "git", "url": "https://github.com/cube-js/cube.git", @@ -19,16 +19,16 @@ "lint:fix": "eslint --fix **/*.js" }, "dependencies": { - "@cubejs-backend/base-driver": "1.7.27", - "@cubejs-backend/query-orchestrator": "1.7.27", - "@cubejs-backend/schema-compiler": "1.7.27", - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/base-driver": "1.7.28", + "@cubejs-backend/query-orchestrator": "1.7.28", + "@cubejs-backend/schema-compiler": "1.7.28", + "@cubejs-backend/shared": "1.7.28", "vertica-nodejs": "^1.0.3" }, "license": "Apache-2.0", "devDependencies": { - "@cubejs-backend/linter": "1.7.27", - "@cubejs-backend/testing-shared": "1.7.27", + "@cubejs-backend/linter": "1.7.28", + "@cubejs-backend/testing-shared": "1.7.28", "@types/jest": "^29", "jest": "^29", "testcontainers": "^10.28.0" diff --git a/rust/cubesql/CHANGELOG.md b/rust/cubesql/CHANGELOG.md index 70640821c287e..0c8e2df1763b1 100644 --- a/rust/cubesql/CHANGELOG.md +++ b/rust/cubesql/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +### Features + +- **cubesql:** Push UNION down to the data source ([#11651](https://github.com/cube-js/cube/issues/11651)) ([2aba3ac](https://github.com/cube-js/cube/commit/2aba3ac6d15b3f6885df952f3b0380dbbbd5a31d)) + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) ### Bug Fixes diff --git a/rust/cubesql/package.json b/rust/cubesql/package.json index 0438d17f69b34..bb8245128fb5b 100644 --- a/rust/cubesql/package.json +++ b/rust/cubesql/package.json @@ -1,6 +1,6 @@ { "name": "@cubejs-backend/cubesql", - "version": "1.7.27", + "version": "1.7.28", "description": "SQL API for Cube as proxy over MySQL protocol.", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" diff --git a/rust/cubestore/CHANGELOG.md b/rust/cubestore/CHANGELOG.md index 81ce3ccd43421..986e4dfbca598 100644 --- a/rust/cubestore/CHANGELOG.md +++ b/rust/cubestore/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.7.28](https://github.com/cube-js/cube/compare/v1.7.27...v1.7.28) (2026-08-26) + +### Bug Fixes + +- **cubestore-driver:** don't fail queries on `write EPIPE`, report over-limit messages readably ([#11490](https://github.com/cube-js/cube/issues/11490)) ([d9ff741](https://github.com/cube-js/cube/commit/d9ff741b991ae9fb0bde5b742f6d514f3a7f94d7)), closes [#11655](https://github.com/cube-js/cube/issues/11655) + ## [1.7.27](https://github.com/cube-js/cube/compare/v1.7.26...v1.7.27) (2026-08-26) ### Bug Fixes diff --git a/rust/cubestore/Cargo.lock b/rust/cubestore/Cargo.lock index 7571451e9c16f..7046d5b330ca3 100644 --- a/rust/cubestore/Cargo.lock +++ b/rust/cubestore/Cargo.lock @@ -1445,7 +1445,7 @@ dependencies = [ [[package]] name = "cubestore" -version = "1.7.27" +version = "1.7.28" dependencies = [ "actix-rt", "anyhow", diff --git a/rust/cubestore/cubestore/Cargo.toml b/rust/cubestore/cubestore/Cargo.toml index 161d1d17d9e30..830d005dad398 100644 --- a/rust/cubestore/cubestore/Cargo.toml +++ b/rust/cubestore/cubestore/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cubestore" -version = "1.7.27" +version = "1.7.28" authors = ["Cube Dev, Inc."] edition = "2021" license = "Apache-2.0" diff --git a/rust/cubestore/package.json b/rust/cubestore/package.json index 5d350f17211d9..19cb3cf9850ea 100644 --- a/rust/cubestore/package.json +++ b/rust/cubestore/package.json @@ -1,6 +1,6 @@ { "name": "@cubejs-backend/cubestore", - "version": "1.7.27", + "version": "1.7.28", "description": "Cube.js pre-aggregation storage layer.", "main": "dist/src/index.js", "typings": "dist/src/index.d.ts", @@ -33,7 +33,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@cubejs-backend/linter": "1.7.27", + "@cubejs-backend/linter": "1.7.28", "@types/jest": "^29", "@types/node": "^18", "jest": "^29", @@ -43,7 +43,7 @@ "access": "public" }, "dependencies": { - "@cubejs-backend/shared": "1.7.27", + "@cubejs-backend/shared": "1.7.28", "@octokit/core": "^3.2.5", "source-map-support": "^0.5.19" },