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/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.
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/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/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/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/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/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[@]}"
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-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-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 8cb61a624913d..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",
@@ -22,14 +22,15 @@
"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",
- "@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",
@@ -40,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-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/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-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\)/);
+ });
+});
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/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(())
+ }
}
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"
},