Releases: Drichards124/dbt-costgate
Release list
dbt-costgate v1.1.0
Added
dbt-costgate config <key>explains one setting and shows the YAML that sets
it, nested under the right section header so it can be pasted as-is. A
section name (dbt-costgate config pricing) prints everything under it, and
the leaf on its own (dbt-costgate config max_pct_increase) works as well as
the dotted key, because the leaf is what the list displays. An unrecognised
name exits 2 and suggests the nearest real one.dbt-costgate config --verboseprints every setting's full explanation —
what the command used to do by default.dbt-costgate configtakes--color auto|always|never, matchingcheck.dbt-costgatewith no arguments now shows how to get started instead of a
list of flags: the two commands that make up the local loop, the two more that
turn it into a pull-request gate, and what to run when it cannot reach
BigQuery.dbt-costgate config --format jsongained asummaryfield — the short
label the list shows. Every existing field is unchanged.
Fixed
dbt-costgate configno longer runs off the side of the terminal. It
printed each setting's whole explanation on one unwrapped line — up to 600
characters — so on an 80-column terminal the text wrapped back to column 0 and
sat flush against the next setting's name, with no blank line between entries.
29 of its 42 lines were too wide to fit. Every screen now wraps to the width of
the terminal it is printing to.
Changed
-
dbt-costgate confignow lists one line per setting instead of twenty
paragraphs. The list is grouped and indented the way the settings nest in
.dbt-costgate.yml, so it doubles as a guide to the file's shape:pricing: region str Force the pricing region usd_per_tib float Flat USD/TiB rate for every regionThe full explanations moved behind two new forms rather than going away.
-
dbt-costgate initsays which setting to uncomment first and no longer
claims every key's note states its default, which was not true of all of them. -
dbt-costgate configand the no-argument screen now follow your terminal
width even when their output is piped or redirected, sodbt-costgate config | lessanddbt-costgate config > settings.txtcome out at the width of the
window you ran them in rather than a fixed 100 columns. SetCOLUMNSto pin it
yourself; with no terminal anywhere — a CI log — it stays at 100.Which report goes where is deliberately unchanged:
dbt-costgate check
still renders at a fixed width whenever its output is not a terminal, so a
report committed to a repo or read back out of a CI log does not depend on the
window that produced it. -
Paragraphs now stop at 100 characters on a wide terminal; tables still use
the full width. Sentences set 200 characters wide are hard to read back —
the eye loses the start of the next line — while a table is read down a column
and a wide window buys it a model name that no longer needs truncating. This
applies to explanatory text everywhere it appears:config, the no-argument
screen, and a report's notes, notices, breach lines and footer.Nothing that gets written to a file or a CI log changes, because those already
rendered at 100. The pricing header line is not capped either — it is a row of
figures rather than a sentence, and capping it would split it across two lines
on a terminal wide enough to show it on one.
dbt-costgate v1.0.5
Fixed
-
A report spanning regions priced differently no longer states one rate as if
it applied to all of them. The header named every region it covered and then
quoted a single rate beside them — whichever region happened to come first. The
built-in table spans 1.8× (US at USD 6.25,southamerica-east1at 11.25), so a
change touching both understated by nearly half in the report's most prominent
line. It now gives a range, and a single figure when every region agrees:dbt-costgate — region: EU, US, asia-northeast1 · on-demand USD 6.25–7.50/TiB · built-in tableThe per-region footer was always correct; only the summary above it was not.
-
An unusual model name no longer breaks the report. A name containing a
newline split the row in half — in the terminal table and the pull-request
comment. A name containing|spilled extra cells across the markdown table
and shifted every figure after it into the wrong column. A name containing an
escape character passed straight through to the terminal, so it could recolour
everything printed after it.These are reachable rather than theoretical: a dbt model name comes from a
filename, and a filename may contain any of them. Control characters are now
shown as a space and pipes are escaped in markdown. The name is unchanged
everywhere it is matched —exclude,warn_onlyandrenamesstill compare
against exactly what the manifest holds, and--format jsonstill reports it
verbatim.
dbt-costgate v1.0.4
Fixed
-
A very large change no longer produces a pull-request comment with no verdict
in it. GitHub rejects a comment over 65,536 characters, so dbt-costgate cuts
an oversized one to fit — from the end, which is where the verdict used to sit.
Measured on a change where every model breached: past about 935 models the
comment was a wall of cost rows,_…report truncated._, and noPASSor
FAILanywhere in it. The exit code was still right and CI still blocked; the
part a person reads had lost its conclusion.The comment now leads with the verdict, and no list in it grows without limit.
Changed
-
The pull-request comment reads summary first, evidence after. The verdict
and the net figure come above the table rather than below it:### 💸 dbt-costgate — cost impact of this change (2 models) ❌ **Gate: FAIL** - fct_orders_daily: USD +13.19/run exceeds USD 5.00 **Net increase:** USD 13.19/run · USD 395.70/month | Model | Baseline | This change | … | -
A change touching more than 50 models shows the 50 largest and says how many
it left out —…and 950 more models, not shown — all of them are in--format json``. The same cap applies to the breach list, the per-model
caveats and the not-estimated list. Under 50 models, nothing changes: no cap,
no note.Rows are chosen by size, since the report is already ordered most expensive
first. Terminal output is uncapped — it scrolls — and--format json
still carries every model, so nothing is lost, only moved to where it fits.
dbt-costgate v1.0.3
Fixed
-
A negative number in any setting is now refused, instead of quietly turning
the gate off.pricing.usd_per_tib: -6.25— one typed minus sign — made
every cost negative, so all three money thresholds stopped firing while the
report still looked ordinary. A 3 TiB model against a USD 10 ceiling went from
GATE: FAIL/ exit 1 toGATE: PASS/ exit 0. A negativerun_frequencydid
the same to the per-month threshold.Ten of the twelve numeric settings accepted a negative value; only
pricing.regionsandpricing.free_tib_per_monthrefused one. All twelve now
do, on the command line as well as in the file:dbt-costgate: .dbt-costgate.yml: pricing.usd_per_tib: must not be negative, got -6.25. dbt-costgate: --usd-per-tib: must not be negative, got -6.25.Zero is unaffected — it still means flat-rate slots for a rate, and a
declared-but-spent allowance for the free tier. If you have a negative value in
a config today, the run now stops at exit 2 and names the key rather than
reporting a number nobody should act on. -
A run frequency written as a fraction is refused rather than silently
rounded down.run_frequency.default: 3.7became3, so every monthly
figure was a fifth too low and nothing said so.
Changed
-
An unreadable config file now says what is wrong and where. The message
used to carry four lines of parser internals — including a caret diagram and
<unicode string>— flattened onto one line, which buried the only part worth
reading. Now:dbt-costgate: .dbt-costgate.yml is not valid YAML: expected ',' or ']', but got '<stream end>' (line 3, column 1). -
Config error messages read as sentences:
must not be negativerather than
must be >= 0, and the few that were missing a full stop have one.
dbt-costgate v1.0.2
Changed
-
The line about a resource dbt-costgate does not price no longer says its one
fact twice. It read "country_codes changed but is not priced — seeds are not
priced". The first half now says what happened and the second says why:dbt-costgate: country_codes changed but is not in the report — seeds are not priced.Same for the
--selectform ("… was selected but is not in the report — …").
Grep foris not pricedstill matches; grep for the full old sentence does not.
It is also the more accurate wording for an ephemeral model, whose cost really
is priced — in the rows of the models that inline it.
Fixed
- A changed analysis is now told that "analyses are not priced", rather than
"analysiss are not priced". The plural was built by adding a letter. - A changed Python model's explanation no longer runs two dashes together.
It was "… is not priced — Python models are not priced — dbt-costgate
estimates SQL scans"; the second dash is now a semicolon.
Docs
- New: What dbt-costgate does not price.
Seeds, snapshots, tests, analyses, hooks, Python models and ephemeral models
get no row in the report, and the tool has always said so on stderr — but no
shipped page explained why, or what each one actually costs. The section names
all seven and is blunt about the part that matters: "not priced" is not
"free." A snapshot is aMERGEand a test is aSELECT; both scan real
bytes that dbt-costgate does not put a number on. Ephemeral models are the
exception — their cost is counted, in the rows of the models that inline them. - Materialized views and BigQuery scripts are now documented, in both the
usage accuracy notes and the explained-page caveats. Their siblings were
already there; these two were not. A materialized view's figure is the cost of
building it once, and BigQuery bills each automatic refresh on top. A model
usingDECLAREorBEGINreads low — the only caveat on the page that
errs that way — because BigQuery can only price the statements it can resolve
before the script runs.
dbt-costgate v1.0.1
Fixed
-
--selectno longer throws away the whole report because one name cannot be
priced. Naming a seed, a snapshot or an ephemeral model alongside real models
used to end the run at exit 2 with nothing printed — even when sixteen other
models had answers. That is reachable from an ordinary CI line, because
dbt ls --select state:modified --resource-type modelincludes ephemerals.Those names are now reported on stderr and the run carries on, which is what
the change-detection path has always done:dbt-costgate: int_names_00 was selected but is not priced — ephemeral models have no relation of their own; their SQL is inlined into the models that select from them, and the cost shows up there.Two things deliberately did not change. A
--selectnaming only unpriced
nodes is still exit 2 — nothing was gated, and that has to stay loud. A name
nobody recognises is still exit 2 with a spelling suggestion, so a stale list
cannot quietly check nothing. -
A BigQuery outage no longer hangs your CI job for up to forty minutes per
model.deadline_seconds(default 60) was bounding nothing.Client.query
takes two independent retries and dbt-costgate set only one, leaving
job_retryat its 2400-second default to re-drive the whole thing underneath.
Measured against real BigQuery, a 5-second deadline ran for 179 seconds over
42 attempts. It now gives up inside the deadline — the same case takes 2.1
seconds. -
A dry-run that BigQuery could not serve now says so. When the retries run
out, the client raises aRetryErrorwrapping the real failure, which fell
through every check and was reported asthe dry-run failed. You now get the
message that was always meant for this:not estimated — BigQuery was unavailable and the retries ran outThis affects the reported reason only. That kind was already treated as an
operational failure, so no exit code or verdict changes.
dbt-costgate v1.0.0
1.0 because the last thing standing in its way is done. The v0.11.0 notes
said the number was blocked on one thing: every test faked
google.cloud.bigquery.Client, so the one part of this tool that talks to a
warehouse had never met a warehouse. It has now — see
docs/qa/live-bigquery-2026-07-27.md. The
dry-run response parses, the error classification holds against live BigQuery
exceptions, and an end-to-end run's byte counts match a direct dry-run exactly.
It also turned up the bug below, which is the argument for doing it.
Nothing here changes a gated figure, a threshold, an exit code or a verdict.
1.0 is a statement about confidence and about the promise that comes with it —
from here, breaking changes wait for 2.0.
Fixed
-
A mistyped dataset name no longer tells you to log in again. BigQuery
answers403— not404— for a dataset or project that does not exist, so a
typo used to end the run withcould not estimate any model (check credentials/permissions). Try gcloud auth application-default loginand exit
2. The exit code is unchanged, but both messages now name the other cause:not estimated — BigQuery refused the dry-run — either it is not allowed, or the dataset or project does not existCheck the names in your
--selectand your model SQL before reaching for IAM. -
max_tib_totalandmax_usd_totalbreaches now name numbers you can tell
apart. Both sides were rendered at two decimal places, so a small cap
produced a breach line that compared a number to itself:- fct_names: 0.00 TiB/run exceeds cap 0.00 TiB - fct_names: USD 0.00/run exceeds cap USD 0.00A 1 GiB ceiling (
max_tib_total: 0.001) is an ordinary thing to set and lands
right in that range. Byte figures now use the same units as the table, and
money widens its decimals only when two places would round an amount away:- fct_names: 147.61 MiB/run exceeds cap 104.86 MiB - fct_names: USD 0.0009/run exceeds cap USD 0.0001Ordinary caps are unchanged —
USD 0.84/run exceeds cap USD 0.50still reads
at two places. Only the wording moved; gating, exit codes and verdicts are
identical.
dbt-costgate v0.11.0
Added
-
You can declare your BigQuery free tier.
pricing.free_tib_per_monthtells
dbt-costgate what your on-demand allowance is, and the report shows where the
change lands against it:pricing: free_tib_per_month: 1 run_frequency: default: 30 # required — an allowance is per month, so it needs a month
dbt-costgate — region: US · on-demand USD 6.25/TiB · built-in table · first 1 TiB/month free Net increase: USD 13.19/run · USD 395.63/month Monthly scan for these models: 85.35 TiB — past the 1 TiB/month you declared freeIt subtracts nothing. No cost figure, threshold, verdict or exit code
changes — set it and the gate behaves exactly as it did. The allowance belongs
to your whole billing account and is drawn down by every other query anyone
runs, which dbt-costgate cannot see, so deducting it would mean assuming it is
still unspent. A gate that forgives the first TiB of a regression on an
unverified assumption is worse than one that over-reports honestly.Three things worth knowing before you set it. "For these models" is literal —
a pull request covers a handful of models, so the figure is not your project's
monthly scan and certainly not your account's. Without arun_frequencythere
is no monthly figure to compare against, and the new
free-tier-needs-run-frequencynotice says so rather than letting the setting
sit there doing nothing. And the tier is an on-demand allowance, so all of this
is suppressed under capacity/Editions pricing.Unset — the default — changes nothing anywhere.
-
--format jsongains two fields:pricing.free_tib_per_month(what you
declared, ornull) andnet.monthly_scan_bytes(raw bytes these models are
projected to scan in a month, ornullwhen no run frequency is set). Unlike
the othernetfigures, the monthly total is absolute rather than a difference,
so it is present in absolute mode too.
A manual QA pass drove the packaged CLI end to end against a real dbt project
and found 21 defects. This release fixes all of them, and rebuilds the terminal
report around a readable table.
Read the breaking changes first if you run dbt-costgate in CI — a gate that
could not check a model now fails instead of passing, and that is deliberate.
Breaking
-
A gate that could not check a model now fails the run (exit 1). Previously
it reportedGATE: PASSand exit 0. There were four ways into this and all of
them are ordinary mistakes: a baseline compiled a different way from the
branch, a model whose dry-run returned no size, a baseline with no compiled
SQL, and a run where nothing at all could be estimated. The report warned in
some of these cases, but CI reads the exit code.In practice this fires when a threshold is configured and a model could not be
measured. If that is expected for a particular model — an external table your
service account cannot see, say — accept it by name:exclude: - external_events
To stop blocking entirely while you sort it out,
fail_on: neverreports the
breaches and exits 0. There is no new setting to learn.One case fails regardless of thresholds: a run where every selected model
failed to estimate.PASSthere does not mean "I checked and found nothing
wrong" — it means the gate never ran. The usual causes are an unbuilt dev
schema, the wrong--project, or a deferred build that never happened. -
A config error now exits 2 instead of 1. ADR-0008 reserves 1 for a
threshold breach; a malformed.dbt-costgate.ymlwas exiting 1 with a Python
stack trace, so CI reported a YAML typo as a cost regression. A failed
--outputwrite moved for the same reason. -
.dbt-costgate.ymlis now validated, and rejects what it used to ignore.
A file that was quietly half-applied will now fail with a message naming the
key. Three things change:exclude: my_modelwritten as a bare string used to become a list of single
characters and match nothing. A scalar is now read as a one-item list, so
this works as written.fail_on: nois YAML for the boolean false. It matched neithernevernor
warnand fell through to the strictest setting — the opposite of what
someone writing "no" means.fail_onandreport.formatare now checked
against their allowed values.- An unknown key is refused, with the nearest documented key as a hint. A typo
likethresholds.max_usd_totlused to leave you with no threshold at all.
-
A manifest compiled for another warehouse is refused (exit 2). Nothing
checkedmetadata.adapter_type, so pointing dbt-costgate at a Snowflake,
Postgres or duckdb project produced confident BigQuery dollar figures for SQL
BigQuery was never going to run. Manifests without the field still run.
Changed
-
The terminal report is a table. It used to write each model as a sentence,
so figures started at a different column on every row, nothing lined up, and a
priced diff row ran to 108 characters and wrapped:fct_orders_daily (full-refresh): 819.20 GiB → 2.91 TiB +264% USD +13.19/run USD +316.46/month (24 runs)Now:
MODEL BASELINE CURRENT Δ % Δ / RUN Δ / MONTH RUNS ──────────────── ──────────── ────────── ────────── ───── ────────── ─────────── ──── fct_orders_daily full-refresh 819.20 GiB 2.91 TiB +264% USD +13.19 USD +395.63 30Per-model warnings moved below the table, into a
NOTESblock keyed by model.
Nothing was removed —(24 runs)became theRUNScolumn.It adapts to the terminal width, giving up columns in a fixed order and saying
which it hid; the model name and the per-run cost are never among them. Below
60 columns it prints one block per model instead. Output redirected to a file
or a pipe always renders at a fixed width, so a captured report does not depend
on the window that produced it. -
Percentages are printed at the precision they need. A 0.4% increase over a
max_pct_increase: 0.3limit used to produce the breach line+0% exceeds 0%,
which reads like a bug rather than a correct failure. -
Unpriced (slot-priced) reports are ordered by size. With every rate at 0
the sort had nothing to work with and rows came out in arrival order. -
The
incrementalandfull-refreshfootnotes say which figure is the big
one. Every fact in the old wording was correct — "the figure is one run
against the table as already built, so it does not gate rebuild cost" — and it
took a second read to work out that the number on screen is the cheap case and
the expensive one is not in the report at all. Each now says what the figure
is, what it is not, and which of the two is larger, in that order:incremental — rows tagged incremental show one run against a table that
already exists. A full rebuild scans far more, and nothing here measures it,
so no threshold on this report can catch a rebuild getting expensive.
Added
-
--color auto|always|never(defaultauto): colour when stdout is a
terminal, off when piped, and off wheneverNO_COLORis set. -
Deleting a model is reported as the saving it is. Removing a model is the
most direct cost reduction a change can make, and it produced no row and no
credit — a branch deleting a 411 GiB/run model reportedNet change: none.
Deleted models are dry-run from the baseline, taggeddeleted, and never
gated: a removal cannot raise cost. -
A change to an ephemeral model now selects the models that inline it. Its
SQL ends up inside them, so widening a filter there is a real cost change —
but on the local path, which is what the pre-commit hook runs, it selected
nothing at all. -
A changed seed, snapshot, Python model or ephemeral model is named. These
are out of scope for pricing, which is fine; being indistinguishable from
"nothing changed" is not, and a snapshot really does run aMERGE. -
A
--selectname that matches nothing is an error (exit 2), listing the
names with a suggestion for near misses, and saying when the model exists but
is a kind dbt-costgate does not price. It used to select nothing in silence, so
a CI job building its list fromdbt lschecked nothing the day that list went
stale. -
A materialized view says its figure is not the recurring cost. It is priced
like a plain view, but BigQuery bills each automatic refresh separately. -
New notice
new-models-not-percentage-gated.max_pct_increaseneeds a
before and an after, so it cannot cover a model that has no baseline. If it is
your only threshold, the report now names the new models that went through
ungated and points atmax_usd_total/max_tib_total, which need no
baseline. Silence it like any other notice. -
JSON gains
skip_reasonandis_deletedper model.gateablealone could
not tell a consumer whether you excluded a model or the gate could not measure
it, and those mean opposite things on a dashboard.
Fixed
-
A model that scanned 0 B on the baseline now breaches
max_pct_increase.
There is no ratio to a zero baseline, so the threshold silently did not apply
to exactly the model it should catch hardest. -
The net line no longer totals a comparison the report just called invalid.
Having warned that a model's two figures cannot be subtracted, it went on to
headline the subtraction. Naming that model inexclude:does not bring the
total back either — an exclusion says "do not fail the build over this", not
"the two figures are comparable after all". -
A permanent SQL error is no longer reported as a transient one. The status
code was matched anywhere in the message, so400 Syntax error … at [500:3]
came back as "BigQuery was unavailable and the retries ran ...
dbt-costgate v0.10.0
Security
-
Reports no longer reproduce BigQuery's error message. A model that could
not be dry-run is now described by what went wrong, not by what BigQuery said:> • **fct_orders** — not estimated — BigQuery rejected the compiled SQLwhere it previously printed the message verbatim. BigQuery quotes the query it
was given, and compiled SQL can embed secrets templated throughenv_var()or
vars — the same reason reports have always excluded compiled SQL. Because a
report becomes a pull-request comment, that message could carry query text,
dataset and table names, and the service-account identity into a comment that
is public on a public repository.The full message is still available, printed to stderr and so kept in the
job log:dbt-costgate: fct_orders: invalid_sql: 400 Syntax error: Unexpected ...This changes report text in all three formats, including the JSON
error
field. If you match on that string, match on the new wording — or read
models[].erroralongside the job log rather than parsing the warehouse
message out of a comment.
Fixed
- A report long enough to be truncated now posts. The sticky comment is
capped at GitHub's size limit; the cut could land inside a multi-byte
character, and the resulting comment was rejected. It is now shortened to a
whole line.
dbt-costgate v0.9.0
Added
-
models[].basisin the JSON report —full_refresh,incremental_form,
direct, ornullwhen it could not be established. It says which query shape
was dry-run, and so whether that model's figure is a rebuild or a single run.
is_incrementalcannot answer this: it is true for both. Additive — every
existing field is unchanged. -
dbt-costgate init— writes a starter.dbt-costgate.yml:dbt-costgate init
Config discovery has always worked, but nothing told you to create the file,
and the documented example had every setting filled in — copy it and you had a
configured project rather than a starting point. The written file documents
every setting with its default, its type and an example value, and leaves all
of them commented out, so it changes nothing until you uncomment one. It
refuses to overwrite an existing config, including one under either of the
other discovered names, and takes--project-dirwhen your dbt project is not
the directory you are standing in. -
A published container image, so CI that isn't GitHub Actions can run the
same check without a Python environment of its own:docker pull ghcr.io/drichards124/dbt-costgate:v0.9.0 docker run --rm -v "$PWD:/workspace" ghcr.io/drichards124/dbt-costgate:v0.9.0 checkEvery release publishes both
:vX.Y.Zand:latest. Pin the version tag —
:latestmoves under you, which is the one thing you do not want from the job
that decides whether a pull request merges. The repository also ships the
Dockerfileitself, so you can build it and push it to your own registry:docker build -t dbt-costgate . docker run --rm -v "$PWD:/workspace" dbt-costgate check
The image runs as a non-root user, mounts your project at
/workspace, and
contains dbt-costgate only — not dbt, so compile in the image you already use
for that and hand this one thetarget/. The
usage guide
has a GitLab pipeline that authenticates keylessly, with no service-account key
anywhere. A published image onghcr.iois wired but not switched on yet;
build it yourself or push it to your own registry until it lands. -
A
pre-commithook, iddbt-costgate. If your team runs
pre-commit, adding this repository to your
.pre-commit-config.yamlcatches an expensive change on your own machine
rather than in review. The config block to copy is in the
usage guide.It runs at the pre-push stage, so install it with the stage named:
pre-commit install --hook-type pre-push
A plain
pre-commit installwill not fire it. Pre-push rather than pre-commit
because the check needs a compiled target and a BigQuery round-trip per changed
model — too slow to sit in front of every commit. It needs the same two things
the CLI does (dbt compilefirst, BigQuery via ADC), and everycheckflag is
available throughargs:. Requires pre-commit 3.2.0 or newer.
Changed
-
The incremental caveat is now one footnote instead of one line per model.
A change touching five incrementals printed the same sentence five times,
which pushed the caveats that are about a specific model — a dynamic filter,
a missing baseline — under a wall of repeats. Reports now tag the rows and
explain the tag once:fct_orders_daily (full-refresh): 819.20 GiB → 2.91 TiB +264% fct_events_hourly (full-refresh): 40.10 GiB → 44.02 GiB +10% ⚠ full-refresh — for the rows tagged above, the figure is the full-refresh scan, not an incremental run.The per-row
full-refreshtag is unchanged, so you can still see exactly
which models it covers, and no other warning is collapsed. If you parse the
terminal or markdown output, note the stringincremental — figure is the full-refresh scanno longer appears in either. The JSON payload is
unchanged —models[].warningsstill carries that warning per model, since
a machine reader has no repetition problem to solve. -
Priced reports now disclose the free tier they do not deduct, in the footer
beside the rate they used:Priced from the first byte scanned: BigQuery's 1 TiB/month on-demand free tier is per billing account, so it is disclosed here and never deducted.Not new behaviour — costs have always been priced from the first byte — but it
was stated only in the docs, which is the wrong place for it: someone comparing
a report against a bill has the report in front of them. The allowance is drawn
down by every other query the billing account runs that month, which a dry-run
cannot see, so deducting it would mean guessing. A figure therefore reads high
by at most one TiB's worth, and a gate that over-reports is safer than one that
lets a regression through.Not configurable, by design: a setting could only mean "assume the tier is
still unspent", a claim about the whole billing account this tool cannot check.
The line does not appear when you have setpricing.usd_per_tib: 0.00— that
report quotes no money for it to adjust, and the tier is an on-demand allowance
that does not apply under capacity/Editions pricing at all. Nothing about the
gate, the breaches or the exit code changes; if you parse reports, note this
adds a line to the terminal footer and a<br/>segment to the markdown one.
The JSON payload is unchanged.The docs previously called the free tier "not modeled by default", which
implied a setting that has never existed. That wording is gone. -
The docs now say plainly that dbt-costgate prices compute, not storage.
BigQuery meters the two separately, and a dry-run reports the bytes a query
would scan — a compute figure that carries no storage information. Nothing
about what the tool measures has changed; it is now stated as the scope it has
always been, under
what it will not do,
with the case worth knowing called out: aviewbecoming atable, or an
incrementalbecoming a fulltable, moves real money on a meter nothing here
watches.
Fixed
-
A baseline with no compiled SQL is no longer reported as a basis mismatch.
When the baseline manifest had no compiled code for a model, it was still
assigned a query shape — and if your branch happened to compile to the other
one, the report said:⚠ mixed basis — baseline is full_refresh, current is incremental_form; recompile the baseline the same waynaming a shape for SQL that was never compiled, directly beside the warning
saying the baseline had no compiled SQL at all. Recompiling could not fix it,
because the mismatch was not real.Such a model is still reported and still not gated — with no baseline
bytes, the whole current scan reads as an increase, and a threshold firing on
that is firing on a missing measurement rather than a regression. That
outcome is now decided where the missing baseline is detected. Previously it
fell out of the bogus mismatch, which meant it only applied when your branch
compiled to the other shape: the same missing baseline gated or did not gate
depending on something unrelated to it. If you have such a model, expect the
spuriousmixed basisline to disappear and the gating to stay off in cases
where it was previously inconsistent.Unestimated rows also no longer carry a
full-refresh/incrementaltag,
andmodels[].basisisnullfor them in JSON — there is no figure for a
basis to describe. -
An incremental model compiled against its existing table is no longer
labelledfull-refresh. An incremental has two prices — the cost to rebuild
the table, and the cost of one run against the table as it already stands —
and which one a dry-run measures is decided by how the model was compiled.
Every incremental row was taggedfull-refreshand told "figure is the
full-refresh scan" regardless, so a figure that was one incremental run read
as rebuild cost. On a large fact table those differ by orders of magnitude,
and the mislabelled one reads low.This was not a rare case: a prod-run manifest captures incrementals in their
incremental form, which the usage guide already documents.Rows are now labelled from the basis actually measured —
full-refreshor the
newincrementaltag — with a footnote under the table for each one present:fct_orders_daily (incremental): 92.16 GiB → 112.64 GiB +22% USD +0.13/run ⚠ incremental — for the rows tagged above, the figure is one run against the table as already built, so it does not gate rebuild cost.If you have a
max_usd_total/max_tib_totalceiling on an incremental
believing it capped rebuild cost, check the tag: on anincrementalrow it
does not, and never did — the label was what said otherwise. The same applies
torun_frequency, which should count rebuilds for afull-refreshrow and
runs for anincrementalone.Two things change in output. The
incrementaltag is new, so anything matching
on the literalfull-refreshwill no longer see these rows. And in JSON,
models[].warningscarries a different sentence for an incremental-form model.
models[].is_incrementalis unchanged — it is true for both shapes, which
is why it could never have answered this.