Repository navigation
docs: DuckDB 1.x to 2.x upgrade notes, and what a project's .env may set (Trello 1062, 845) - #166
Merged
Merged
Conversation
0.22.0 raised the local engine's DuckDB range to `duckdb>=1.5.0` with no upper bound, so `pip install -U "data-product-forge[local]"` can resolve DuckDB 2.x — and so can a rebuilt CI image or another tool sharing the environment, without anyone deciding to upgrade. The notes were never written, so nothing told a reader what that costs. A new "DuckDB 1.x to 2.x" section in the upgrade guide, sourced from forge-cli's 0.22.0 upgrade notes and `Fixed` entries and from the code (`build_runners/duckdb/runner.py`, `build_runners/_schema_evolution.py`, `providers/_duckdb_sandbox.py`): - The database-file split. A file another 2.x tool wrote — dbt-duckdb, dlt, or forge-cli 0.21.0 or older on DuckDB 2.0 — does not open on 1.5. A file forge-cli itself writes on 2.x is in the format 1.5 writes, which 1.0 and later open, and an existing file keeps its format. The conversion SQL is on the page. - Contract SQL runs on the installed DuckDB, and 2.0 changed some: `x -> x + 1` is an error, and integer `//` and `%` by zero raise instead of returning NULL. - A DuckDB acquisition build now resolves a Postgres source's `connection.secretRef`, so one that does not resolve on the apply host fails the build where libpq's `PGPASSWORD` used to cover for it. - Python 3.10 installs stay on 1.5.x, because DuckDB 2.0 needs 3.11. Then the two drift defects, as shipping in the release after 0.22.0 (forge-cli #749, on main). The page names the release rather than a number, because the number is not settled. - An incremental timestamp cursor written on 1.5 and read on 2.0 silently dropped or duplicated rows: the cursor is text in an untyped literal, and a `Z` timestamp binds as TIMESTAMP on 1.5 and TIMESTAMP WITH TIME ZONE on 2.0, where a naive literal is read in the host's zone. A run landed 5 of 6 rows at exit 0 with the stream `succeeded`. It now compares instants. No state file needs rewriting, only a column type `epoch_us` accepts takes that path, and pinning the session zone was rejected because `TimeZone` is ICU-backed and the sandbox forbids autoloading extensions — a pin would fail the connection in exactly the hardened environments the sandbox is for. The accepted cost, lost predicate pushdown, is on the page too. - A pipeline discovered on 1.5 failed `SchemaDriftError` on 2.0 because the gate compared type names, and 2.0 infers HUGEINT where 1.5 infers DOUBLE. Those are now one physical type under two names, with the widening/narrowing ladder unchanged and a note that the stored width does move, so the precision change is reported rather than hidden. - `fluid init --discover` emitted contracts its own `fluid validate` rejected; 35 of DuckDB's type names are rejected by the schema, not the two reported, and two of those failed on 1.5 as well. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ello 845)
Advisory B of 0.20.0 stopped a project's `.env`, `.env.<environment>` and
`.env.local` from setting operator-only variables, and nothing on the
site said so. A reader whose committed `.env` carried
`FLUID_DUCKDB_ALLOWED_DIRS` or `HTTPS_PROXY` got a warning naming it and
no page explaining why, or where to put it instead.
A new page, `advanced/project-dotenv-trust.md`, written from
`credentials/protected_env.py`, `credentials/dotenv_store.py`,
`cli/_common.hydrate_dotenv`, `build_runners/dlt/runner.py` and the
`tests/test_dotenv_*` suite:
- Which files load, in which order, and that only the three project
files are filtered — `FLUID_SECRETS_FILE` counts only when the
operator exported it, so a path a project file sets is ignored.
- Credentials still load, and so do the values the contract already
decides and the budgets and timeouts. Five categories do not: widening
a sandbox or confinement root, moving where trusted config is read or
state written, changing authentication or trust, changing where data
and credentials go, and choosing which program runs.
- The shape rule, so an unlisted `FLUID_*` name whose suffix or fragment
says it decides something is refused before anyone lists it.
- Both warnings as they are actually worded, naming variables and files
and never values, and that a name the operator already exported is
left out because their value is the one in effect.
- The pairing rule for third-party endpoints, including why a decoy
credential pairs nothing: a project credential a reader consults after
the keyring, or never, is not what goes out.
- dlt destinations are default-deny at any depth and under every section
path dlt reads, with the in-account exceptions as a table
(`runner._DLT_IN_ACCOUNT_FIELDS`).
- Why dbt and litellm no longer re-read the project's files, and the
three ways to get a variable to dbt.
Values are read literally and names compared upper-cased; both are on
the page, because `${VAR}` expansion and a case-differing name were how
the original bug was reached.
The DuckDB sandbox page gains the 1.5.x caveat: `lock_configuration`
does not hold there for DuckDB's logging settings reached as a table
function rather than with `SET`, and the sandbox's own path refusal then
aborts the process from DuckDB's logger write path. It widens no grant —
availability, not confidentiality — and the engine's own SQL never
reaches it; what is exposed is a surface running SQL somebody else
wrote, such as the MCP output port's `query_sql` under
`policy.allow_free_form_sql`. DuckDB 2.0 refuses it cleanly, so the
mitigations are 2.x or leaving that policy off. The bullet states the
limitation and the fix and deliberately spells no reproduction.
`FLUID_DUCKDB_ALLOWED_DIRS` now says on the sandbox page that a project
`.env` cannot set it either, which is the consequence a reader hits
first, and the environment-variables page carries the same warning at
the top. Sidebar and folder index updated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two release tasks that were never written up. Branched off
agenticstiger/main, not on top of #165 — one commit per card, and the two branches do not touch the same lines (git merge-treereports no conflict either way round, so they can land in either order).Closes Trello 1062 (P1) and Trello 845 (P2).
Trello 1062 — DuckDB 1.x to 2.x upgrade notes
0.22.0raised the local engine toduckdb>=1.5.0with no upper bound. The reason this is P1 is that nobody has to decide to upgrade:pip install -U "data-product-forge[local]"on a fresh environment, a rebuilt CI image, or another tool in the same environment pulling DuckDB 2.x in will all do it. A new DuckDB 1.x to 2.x section inupgrading.mdsays what that costs, with the table-of-three-columns shape the rest of the page uses.Sourced from
CHANGELOG.md's0.22.0upgrade notes andFixedentries, and checked againstbuild_runners/duckdb/runner.py,build_runners/_schema_evolution.pyandproviders/_duckdb_sandbox.py.What breaks for someone still on 1.x, and what they change
0.21.0or older running on DuckDB 2.0 — cannot be opened on 1.5. The conversion (ATTACH … (STORAGE_VERSION 'v1.0.0'),COPY FROM DATABASE) is on the page. A file forge-cli itself writes on 2.x is in the format 1.5 writes, which DuckDB 1.0 and later open, and a file that already exists keeps its format — so a type needing a newer format, such asVARIANT, is refused in it on 2.x exactly as on 1.5.x -> x + 1is an error (writelambda x: x + 1), and integer//and%by zero raise instead of returning NULL (wrap inTRY(...)).connection.secretRef. It ignored it before and libpq'sPGPASSWORDor~/.pgpasssupplied the password, so a reference that does not resolve on the host runningfluid applynow fails the build. Remove thesecretRefto keep usingPGPASSWORD; an inlinepasswordstill wins.The type and time-zone drift that landed after the 0.22.0 cut
Written as shipping in the release after
0.22.0(forge-cli #749, onmain). The page names the release rather than a number, and says in the admonition that the number is not settled — so there is nothing to go back and edit except that one sentence.WHERE <field> > '<text>'as an untyped literal, and the two versions disagree about the type it is coerced to:read_json_autoover a timestamp carryingZgivesTIMESTAMPon 1.5 andTIMESTAMP WITH TIME ZONEon 2.0, where a naive literal is read in the session time zone — the host's. On a host at a non-zero UTC offset the>boundary moved: a run that wrote its cursor on 1.5 and continued on 2.0 landed 5 of 6 rows and advanced the cursor past the row it skipped, at exit code 0 with the stream recordedsucceeded. Both versions satisfyduckdb>=1.5.0, so one install can write state another reads. CSV sources were never affected.SchemaDriftErroron 2.0 on a false positive: the gate compared type names, and 2.0 infersHUGEINTwhere 1.5 infersDOUBLEfor the same integer literal, andTIMESTAMP WITH TIME ZONEwhere 1.5 infersTIMESTAMP. Upgrading DuckDB — no contract edit, no new data — broke a working pipeline.Your two accuracy notes are both on the page, and both check out in the code:
_canonical_cursor_textnormalises a cursor a shipped 2.0 install already wrote, because that text carries an explicit offset._cursor_comparison's own docstring gives the reason:TimeZoneis ICU-backed, the sandbox forbids autoloading extensions, and without ICU DuckDB does not recognise the option at all, so a pin would fail the connection in exactly the hardened environments the sandbox exists for.Three things the page adds that you did not ask for, because leaving them out would have overstated the fix:
epoch_usaccepts (_EPOCH_US_TYPES: the TIMESTAMP family,DATE,TIME). Integer, date-as-integer and opaque string cursors keep the plain comparison, sinceepoch_usraisesBinderExceptiononVARCHAR,INTEGERandDECIMAL. The choice is made from the column's type, never the cursor text's shape.DOUBLEandHUGEINTare not value-identical — for the same literal 1.5 landed a lossy 17-significant-digit float where 2.0 lands the exact 128-bit integer — so the non-failing path reports that the physical type moved. The widening/narrowing ladder is unchanged; the family check runs after it, sotype_widenedandtype_narrowedstill fire for every ordered numeric pair.#749 also fixes a third defect you did not mention, and it is on the page:
fluid init --discoveremitted contracts its ownfluid validaterejected (type: hugeint,type: '"null"'). Validating DuckDB's ownduckdb_types()against the bundled schema found 35 rejected names, not the two reported, anduintegerandinteger[]were rejected on 1.5 as well — so this was never only a 2.0 problem. Discovered types are canonicalised at the emitter, an unsigned or 128-bit type mapping to a width that holds its range (uinteger→bigint,hugeint→numeric).Trello 845 — what a project's
.envmay setNew page:
advanced/project-dotenv-trust.md, in Govern & secure in the sidebar and in the folder index.Written from the code before a word of prose:
credentials/protected_env.py,credentials/dotenv_store.py,cli/_common.hydrate_dotenv,build_runners/dlt/runner.py, and thetests/test_dotenv_*suite including the two you named.FLUID_SECRETS_FILEcounts only when the operator exported it before the call, so a path a project file sets is ignored and the file it names is never read..env.<environment>is the--envvalue:hydrate_dotenvis called withenvironment=getattr(args, "env", None)fromapplyandpublish.PYTHON_DOTENV_DISABLED,LITELLM_MODE). And what does still load: credentials, the values the contract already decides, the budgets and timeouts.FLUID_*name is protected when its suffix or fragment says it decides something. The page notes_MCP_ENVrather than a bare_ENV, becauseFLUID_ENVis the environment's name and loads.runner._DLT_IN_ACCOUNT_FIELDSrather than from prose.Values read literally (
interpolate=False) and names compared upper-cased are both on the page, because${VAR}expansion and a case-differing name were how the original bug was reached.Cross-links, which is where a reader actually arrives from:
FLUID_DUCKDB_ALLOWED_DIRSon the sandbox page now says a project.envcannot set it either, andenvironment-variables.mdcarries the same warning at the top.The sandbox caveat
A bullet in the DuckDB sandbox page's Limits list.
lock_configurationdoes not hold on DuckDB 1.5.x for DuckDB's logging settings when they are reached as a table function rather than withSET; the sandbox's own path refusal then comes out of DuckDB's logger write path as an uncaught C++ exception that aborts the process (SIGABRT, reproduced on 1.5.1 and 1.5.6). It widens no grant — availability, not confidentiality — and the engine's own SQL never reaches it. What is exposed is a surface that runs SQL somebody else wrote, such as the MCP output port'squery_sqlunderpolicy.allow_free_form_sql, whose SELECT-only gate a table function passes. DuckDB 2.0 refuses it cleanly, so the mitigations are 2.x where you expose free-form SQL, and otherwise leaving that policy off.The bullet states the limitation, the blast radius and the two mitigations, and spells no reproduction.
Premise correction
Trello 845's feature shipped in
0.20.0, not at the0.22.0cut.credentials/protected_env.pywas created on 2026-10-16 (29a46215, a fork merge), which is an ancestor of the0.20.0release commit6b263f04and not of0.19.0's. It is advisory B of that release, GHSA-58px-v64p-6ppm. So it is a genuine leftover — it just has been undocumented for two releases longer than the card implies, and the page is markedSince 0.20.0rather than deferred to the coming release.What is post-
0.22.0there is two more protected names,FLUID_ACQUISITION_SECRET_REF_ALLOWLISTandTESTCONTAINERS_HOST_OVERRIDE(forge-cli #751). They get their own admonition, so they are easy to fold in when the release lands.Trello 1062's premise holds as given: the floor moved in
0.22.0(#740, in that release), and #749's three fixes are onmainafter the cut.Verified locally
Reproduced from the workflow files, on python3.12 with
data-product-forge==0.18.1in a venv:check_cli_docs.py --version-onlycheck_cli_docs.pycheck_providers.pygen_contract_reference.py --checknpm run docs:buildnode scripts/check-dist-links.mjscli-version.jsonis untouched in both commits. Nothing added here says "unreleased", so this branch also passes the release-marker sweep from #165 if that lands first.Not claimed
The page does not say forge-cli validates anything against a released Bitol schema, does not name the coming release's version, and does not document the acquisition
secretRefhost policy itself (#750 / #751) beyond the two variable names above — that is a page of its own once the release is out, and it is on the follow-up list rather than widening this one.