Skip to content

Reference / Authoring

validate_cube_sql

Run candidate cube SQL once, read-only, and report columns, row count, advisories and a size recommendation. Parameters and errors.

Runs a candidate cube SQL once, read-only, against the connection's surface, and returns the column shape, the row count, a capped sample of rows, advisories, and a size recommendation. The row count is exact except in one SQL Server case, described under Caps below, where it is a floor.

The SQL runs under exactly the confinement the refresh cron will later enforce: a single read-only statement, a row and byte cap, and a statement timeout. A query that validates here keeps running unattended.

Behaviour hints for clients: read-only, not open-world.

Parameters

ParameterTypeRequiredNotes
sqlstringyesA single read-only SELECT or WITH ... SELECT, and the shape depends on the connection. Against a warehouse or the uploaded-file source, return one row per underlying record and let Dashies work each number out, because it holds that data and aggregates it when a viewer opens the page: a statement that has already aggregated is aggregated a second time, and a count comes back as the number of groups you made rather than the number of records. Against self, group by your dimension keys and select one aggregate per measure. Never a hardcoded date, since it re-runs unattended, and anchor a relative window to the newest complete period the data itself holds rather than to the wall clock, except on self, whose dates are computed against today.
connectionstringnoself (the default), a warehouse connection id, or the uploaded-file source id (which also needs upload).
uploadstringnoWhich uploaded file to read, as an upload id from create_file_upload. Required when connection is the workspace's uploaded-file source, and refused on any other connection - a warehouse has no uploads. There is no default and the newest upload is not assumed: a later upload would otherwise silently change what this call answers and what a published dashboard reads, so omitting it is refused with the newest ready upload named for you to pass back.
modestringnocube, lattice, rows or hybrid. Selects which advisory rule applies. Omit for a single-mode cube.
stock_columnsarraynoColumns in this SQL that hold a point-in-time stock rather than a per-period flow. Advisory only.
echo_rowsintegernoHow many rows to echo back, minimum 0. Omit for the default sample.
entitlement_keystringnoThe column that decides who sees which rows, for a dataset you intend to give row-level security. Advisory only: it reports when that column already holds more than 100 distinct values in the rows this check read.

Caps

CapValue
Rows requested100000
Rows echoed200
Bytes echoed8192
Statement budget, self8000 ms
Statement budget, warehouse15 s

The row cap and the byte cap on the echo bind independently, whichever comes first. Neither one changes row_count, and rows_truncated is true whenever the echoed sample is shorter than the rows the check read.

row_count is a floor rather than a total on a SQL Server connection whose data Dashies holds, when a statement fills this check's sample. The check reads a bounded sample there and does not count past it, so Rows reads at least <n> and row_count is that <n>. The text says so, reports the size as not measured (size_band and recommended_mode then describe only the rows read), and gives a select count(*) wrapper you can validate to learn the true total. A statement returning that many rows is expected on such a connection and is not a problem: the data stays outside the page and every refresh reads all of it.

rows is a sample, not the cube

Never treat rows as the whole result. When rows_truncated is true you have a prefix, and baking that into a data island ships a truncated dashboard. Publish a small placeholder island and let the first refresh fill it, move a large row-level dataset to Parquet, or coarsen the grain until the whole cube inlines.

That last remedy is the in-file one. A dataset whose rows stay with Dashies ships nothing inside the file, so there is nothing there to make inline, and coarsening its grain hands Dashies a summary to summarize: it holds those rows and works each number out when a reader opens the page, so a statement that has already aggregated is aggregated a second time.

Pass echo_rows: 0 when publishing a spec

On the spec publish path the rows are dead weight: the server seeds the data island from this same SQL, so you never paste rows anywhere. echo_rows: 0 drops the sample while keeping the column shape, row_count, every advisory and the size recommendation. It can only make the result smaller: it is clamped to the server's row limit, and the byte budget still applies regardless.

Returns

Cube SQL is valid for connection "self".
Rows: 96
Columns:
- month: string
- plan: string
- revenue_usd: number

Sample (first 5 of 96 returned):
  {"month":"2026-07-01","plan":"pro","revenue_usd":41230}
  {"month":"2026-07-01","plan":"team","revenue_usd":18720}
  {"month":"2026-07-01","plan":"enterprise","revenue_usd":96400}
  {"month":"2026-06-01","plan":"pro","revenue_usd":39815}
  {"month":"2026-06-01","plan":"team","revenue_usd":17960}

The 96 result rows are in the JSON block below, and the columns above are what your dimensions and measures can be built from. You do not need to copy the rows anywhere: the server re-runs this SQL itself when it publishes.

Size: this result is small enough to travel inside the dashboard (size_band: small, this connection's cap is 100000 rows / 8000000 bytes) - nothing needs to change (recommended_mode: inline).
BEGIN_JSON
{"connection":"self","ok":true,"row_count":96,"rows_truncated":false,"columns":[...],"rows":[...],"warnings":[],"size_band":"small","recommended_mode":"inline"}
END_JSON

The columns and rows arrays are shortened here. The real block carries every column and every echoed row.

When rows_truncated is true and echo_rows was not 0, that guidance line reads instead:

The JSON block below carries the first <k> of <n> result rows - the full set is too large to return here, which is expected and fine. The server re-runs this SQL itself when it publishes, so the sample is only for you to read; use the columns above to see what your dimensions and measures can be built from.

On a SQL Server connection whose row_count is a floor (see Caps), the guidance line and the size line read instead:

The JSON block below carries <k> of the <n> row(s) this check read, and the statement returns at least <n>. You do not need to copy any of them anywhere: the server re-runs this SQL itself when it publishes.

Size: not measured. This check read <n> row(s) of a result that has at least that many, so it cannot say how large the whole result is; the size_band and recommended_mode in the JSON block describe only the rows it read.

Advisories

Two channels, and the difference matters if you are scripting:

  • Warning: ... lines have a machine-readable counterpart in the warnings array.
  • Note: ... lines are text-only. The manifest v3 grain-compiler blessing is a note.

The advisory that fires most often names non-additive aggregates, and there are two of them with different remedies. Which one you get depends on whether you passed mode, so do not go looking for the other one.

With mode: cube, the remedy is a different v4 dataset mode:

For a v4 "cube" dataset this SQL computes non-additive aggregates (avg): a cube dataset re-sums pre-aggregated rows in JS, so these go silently wrong under viewer filters. Use mode "lattice" (exact per cell, no engine to load) when the dimensions are low-cardinality, or mode "rows" (row-level + DuckDB) for row-level detail.

With mode omitted, the advisory stays version-agnostic and the remedy is a manifest version:

This SQL computes non-additive aggregates (avg); it requires manifest v2 (row-level + DuckDB). A manifest v1 (additive cube) publish rejects it: the v1 page re-sums pre-aggregated rows in JS, which silently mis-aggregates non-additive values under viewer filters. For v2, have cube_sql return the underlying rows and declare the metric as a v2 measure (agg: 'count_distinct' / 'median' / 'percentile_cont' / 'avg') so the engine recomputes it with real SQL.

Both are Warning: lines with an entry in warnings. A GROUP BY CUBE shape with per-dimension GROUPING(<dim>) AS __g_<dim> tags is recognised and gets a Note: instead of either, because those same aggregates are exact per lattice cell.

A separate Warning: fires on approximate aggregates under mode: lattice or mode: hybrid, and it is a refusal-in-waiting rather than advice: a lattice REJECTS them at publish, because its guarantee is that every precomputed cell is the exact aggregate for its filter state.

The size recommendation

FieldValues
size_bandsmall, big, extreme
recommended_modeinline, parquet, reject

Act on recommended_mode. inline means bake the rows into the island. parquet means publish a row-level dataset with data.mode: parquet so the scheduled refresh offloads the rows. reject means the cube is too large to serve as it stands, so narrow the window or coarsen the grain.

inline and parquet are the in-file vocabulary, so read recommended_mode against your connection. A dataset whose rows Dashies holds carries no island and declares no offload, so neither describes it. reject does reach it, and the remedy there is the narrower window rather than the coarser grain. See Datasets.

Errors

The shared connection rules, plus:

ConditionText
Missing SQLsql is required
Bad modemode must be one of cube/rows/lattice/hybrid - the v4 dataset mode to validate this SQL for (omit it for a single-mode v1/v2/v3 cube).
Bad stock columnsstock_columns must be an array of non-empty column names - the columns in this SQL that hold a point-in-time STOCK (a level such as ARR, headcount, a balance) rather than a per-period flow.
Bad echo countecho_rows must be a non-negative integer - how many result rows to echo back (0 for none; capped at 200 and by the result byte budget either way).
No columns came backCube SQL ran but returned no columns. A cube needs the grain dimensions plus at least one measure - check your SELECT and GROUP BY.
An unclassified refusal on self or PostgresCube SQL rejected [<sqlstate>]: <message>

check your SELECT and GROUP BY is the server's own wording and it assumes the in-file shape. Against a warehouse or the uploaded-file source the statement need not group at all: it returns one row per underlying record and each measure is declared rather than computed in the SQL. Where it does group, it groups back to one row per record rather than to the grain the dashboard reports at, so there is no reporting-grain GROUP BY for that sentence to be about, and what the refusal means there is that the statement returned no columns at all.

On self and on a Postgres connection, a timeout is classified and names both budgets, so you can tell which one you hit:

Cube SQL timed out against the connection (authoring runs on a 15s budget for a warehouse connection, 8s for self) - it moved more data than the budget allows.

Whether a refusal carries your warehouse's own words depends on the engine.

  • BigQuery, Snowflake, Redshift and Databricks. When the engine refuses the statement itself, you get the engine's own message with nothing added, cut at 2,000 characters. If that answer carries no message, the text is BigQuery rejected the cube SQL, Snowflake rejected the cube SQL, Redshift rejected the cube SQL or Databricks rejected the cube SQL. A failure that is not about the statement, such as a credential or reachability problem, gets a short Dashies sentence naming the kind of failure instead.

  • SQL Server. When SQL Server refuses the statement, you get its own message with nothing added, for example Invalid column name 'amont'. A refusal Dashies makes before the statement reaches SQL Server, such as a column type it cannot read, is a Dashies sentence. When the error number says the statement is not the problem (a lock timeout, a deadlock, a throttle, a database failing over), you get this instead, where <reason> is a short Dashies phrase such as warehouse query timed out:

    SQL Server did not answer for a reason that is not in the SQL (<reason>, SQL Server error <number>). Nothing in the SQL needs to change; run it again.

    When running it again will not help, the second sentence reads Nothing in the SQL needs to change; re-test the connection in the Dashies web app (Data sources).

  • Postgres. You get Cube SQL rejected [<sqlstate>]: <message>, where <message> is Postgres's own message with your connection's host, database name, user name and password (each one three characters or longer) replaced by [redacted] wherever they appear, for example Cube SQL rejected [42703]: column "amont" does not exist. When the check cannot use its own connection to your database and runs the statement through the connection's confined executor instead, Postgres's words do not come back: that executor passes on only the SQLSTATE and a class, so the text reads Cube SQL rejected [<sqlstate>]: warehouse_execute_failed:<class>. A refusal the check recognises, such as a timeout, gets its own sentence instead.

One Databricks sentence gets a note appended. When Databricks refuses with The request could not be processed by the warehouse. and nothing else, that sentence is still surfaced verbatim and a note follows it: the refusal is not necessarily about your SQL; if even select 1 is refused the same way, the SQL warehouse may be unable to start, and an account that has run out of credits is one cause known to produce exactly this refusal; check in your Databricks workspace that the warehouse can start. Every other Databricks message is surfaced exactly as before.

A cube too large to inline exceeds the row or byte cap and comes back as a rejection whose message carries the same size recommendation, labelled an estimate because validation stopped at the cap.

Rate limited at 120 calls per user per minute.

Scope

Connection-scoped, so there is no workspace argument. It accepts any connection belonging to a workspace you are a current member of, plus the built-in self and the shared sample. A warehouse connection requires a paid plan; self does not.

Check it worked

A green validate is not a correct number

This tool proves the SQL runs, returns a shape, and fits the budget. It says nothing about whether the numbers are right. Before you trust a published figure, sum each additive measure over the cube and compare it against an independent aggregate over the un-joined base table. If those disagree, the cube is wrong and every refresh will keep it wrong.

Also confirm rows_truncated is false before pasting rows anywhere, and that row_count matches what you expect from the grain you designed.