Correctness is built into every query.

AI can make assumptions. iDash guarantees accuracy by verifying the calculation logic before showing you the result.

The failure mode

A join can inflate a total.

Join a customer to their orders and the customer appears once per order. Count them and the count is wrong.

Three customers, six orders

Every row this join produces belongs there. That is what makes the wrong total so hard to catch by eye.

customers3 rows, one per person
customer_idchannellifetime_value
c_1041Organic search$6,180
c_1042Paid social$2,940
c_1043Referral$5,120
orders6 rows, one per purchase
order_idcustomer_idnet_total
o_88401c_1041$2,310
o_88402c_1042$2,940
o_88403c_1041$1,940
o_88404c_1043$3,180
o_88405c_1041$1,930
o_88406c_1043$1,940
Joined on customer_id
customers joined to orders6 rows, each customer repeated once per order
customer_idlifetime_valueorder_idnet_total
c_1041$6,180o_88401$2,310
c_1041$6,180Repeatedo_88403$1,940
c_1041$6,180Repeatedo_88405$1,930
c_1042$2,940o_88402$2,940
c_1043$5,120o_88404$3,180
c_1043$5,120Repeatedo_88406$1,940

Sum of lifetime value across the join

$31,720

Three repeated rows carried their customer total a second time.

Sum after dedup on customer_id

$14,240

One row per customer, then add the column up.

One question, two answers

1,842 customers who have placed 9,610 orders between them, asked for lifetime value by acquisition channel.

MetricJoined naivelyVerified by iDash
CustomersCounted once per order9,6101,842
Lifetime valueEach customer summed once per order$47.5M$9.1M
OrdersMeasured on the inflated side, so unaffected9,6109,610

The fix is a plan, not a prompt.

Before any SQL is written, iDash computes a dedup plan from the shape of the aggregation. Same query, same plan, every time.

  1. Read the shape

    The compiler looks at which models are being aggregated, which joins the question needs, and which of those joins multiply rows on the measured side. This is arithmetic on the model graph, not an opinion.

  2. Dedupe on the primary key

    Where a model is inflated, the compiler emits a step that reduces it to one row per primary key before the aggregation happens. The measure is then summed over rows that exist once.

  3. Compile for your dialect

    The plan becomes real SQL for the source you connected: PostgreSQL, MySQL, SQL Server, ClickHouse, Trino or a file. The dialect differences are the engine's problem, not yours.

  4. Check the result again

    A separate rule inspects the generated SQL for fan-out afterwards, without knowing what was asked. Two independent passes have to agree before a number reaches you.

Three questions it will not answer.

A refusal is a feature. Each one names the reason and tells you how to ask a version of the question that can be proved.

  • The join path is ambiguous

    There is more than one way to connect two tables, and the routes disagree. Picking one silently would mean choosing an answer on your behalf.

    What the compiler says

    There are two paths from customers to revenue, one through orders and one through invoices, and they do not agree. Tell me which one this question means.

    Name the path, or curate the relationship in the model so the choice is made once.

  • The table has no primary key

    Deduping means keeping one row per key. With no key there is no definition of the same row, so there is no safe way to collapse the inflation.

    What the compiler says

    The events table has no primary key, so I cannot remove the duplicate rows this join creates. Add a key, or ask for a count of events instead.

    Add a primary key upstream, or ask for a measure that lives on the inflated side.

  • Two models are inflated at once

    One inflated table can be deduped in place. Two independently inflated tables in a single question need a plan that is not built, so the query stops rather than approximating.

    What the compiler says

    Orders and support tickets both multiply the customer row. Ask for one at a time and I can answer both.

    Split the question in two. Both halves compile.

Checked twice, in different places.

The plan is built in the compiler and verified again after the SQL is generated, by a rule that has never seen the question.

Compiled query planVerified

Question

What was revenue by region last quarter?

Resolved

model
orders
measure
sum(orders.net_revenue)
dimension
regions.region_name
grain
order_id

Join path

orderscustomersregions
  • customers.customer_id · many to one
  • regions.region_id · many to one

Primary key dedup applied on order_id

order_items multiplies each order row 3.4 times on average. Revenue is measured on the order grain, so the compiler dedups on the primary key before aggregating.

Compiled plan

WITH orders_dedup AS (
  SELECT DISTINCT ON (o.order_id)
         o.order_id, o.customer_id, o.net_revenue, o.placed_at
    FROM orders o
    JOIN order_items oi ON oi.order_id = o.order_id
)
SELECT r.region_name,
       SUM(od.net_revenue) AS net_revenue
  FROM orders_dedup od
  JOIN customers c ON c.customer_id = od.customer_id
  JOIN regions   r ON r.region_id   = c.region_id
 WHERE od.placed_at >= DATE '2026-05-01'
   AND od.placed_at <  DATE '2026-08-01'
 GROUP BY r.region_name
 ORDER BY net_revenue DESC
  • Join path resolved, 2 hops, no ambiguity
  • Primary key dedup applied on orders.order_id
  • One inflated model in the query, single dedup CTE is sufficient
  • Read only connection, statement timeout 30s
  • 5 rows · 412ms
Compiled query planRefused

Question

What was revenue by campaign last quarter?

Ambiguous join path

Both paths are valid and they return different numbers. Rather than pick one and hand you a number that looks fine, the compiler stops.

  • ordersattributionscampaigns

    last touch

  • orderssessionscampaigns

    session source

Compiler

ambiguous join path from 'orders' to 'campaigns': 2 equally short paths exist. Query one of them directly as the base model instead.

How to get an answer

  • Ask for one of them by name, for example revenue by last touch campaign.
  • Mark one relationship as the default in the semantic model.

Automated calculation checks

Independent query verification

Isolated execution environment

Pre-tested AI models

Every number carries its receipt.

Two labels, on every result. Semantic means the compiler produced it. Raw means it came from fallback SQL. There is no third, quiet option.

VerifiedProduction Postgres · 5 rows · 412ms

semantic

Compiled from verified business definitions

The question was calculated using your verified business definitions and checked for accuracy. This is the default path for standard metrics.

Unverified SQLProduction Postgres

raw

Direct query, clearly labeled

For questions requiring ad-hoc analysis, iDash calculates the answer directly and clearly labels it as an unverified direct query.

What it will never do.

The guarantees are narrow on purpose. Here is what iDash does not claim, so you know exactly what you are trusting.

The join graph comes from your keys. If the graph does not support the question, the answer is a refusal, not an improvisation.

Test it on a number you know.

Connect a read-only replica, ask something you already have the answer to, and compare.

Book a demo