v0.2.51
LATESTTHE RING
Every fix in this release was found by a machine walking between engines, not by a human reading code. The ring is a campaign that visits ordered pairs of engines — DuckDB to Postgres, Postgres to MySQL, DuckDB to Trino — and at each stop runs the entire command surface against real databases: flip the project onto the new engine, parse, seed, build twice, snapshot twice, check idempotence, reconcile row counts AND named values, generate docs, generate sources, generate base views, execute a native statement, retract everything, land a branch. Twenty-seven commands per hop. A hop is green only when all twenty-seven pass; anything less is a bug to fix, not a result to record. Sixteen hops are green so far, and they produced the fifty-odd fixes below — a silent row-duplication bug, a segfault that killed every Python model on macOS, a retract that deleted the project's own source tables, quoting that broke on seven engine-and-role combinations, and a bulk loader that landed different column names than the ordinary one. Most of these had passed every test suite DVT had.
FIXED
- ▸Flipping a project onto an engine that already hosts the model's source no longer silently duplicates every row — for models that declare watermark_column as a literal in their own config() block. De-federating an f_incremental into a native incremental dropped that config, which only the federation engine implements, so the model parsed, compiled and built green while re-appending the full source on every run. The flip now carries the watermark across as an is_incremental() guard, and refuses rather than guessing when it cannot.
- ▸Python models no longer segfault the run on macOS. Any Python model executing after another model had touched DuckDB killed the process outright: pandas 3 routes DataFrame construction through Arrow-backed strings, which crashes once DuckDB has initialised in the same process. The guard is installed wherever DVT touches a driver, so dvt docs generate against a Databricks connection (rc=-11) is fixed by the same change.
- ▸A full dvt retract no longer deletes the tables your project declares as sources. Sweeping every ledger entry dropped the very inputs sources.yml names, and the next build failed 44 federated models on tables DVT itself had just deleted. Declared sources are inputs, not products, and a full retract now spares them — an explicit --select still overrides.
- ▸Cross-engine rendering no longer adds quotes to identifiers you wrote bare. A mixed-case column your engine folded at creation began failing with column "SKU_Code" does not exist against a table whose column really was sku_code. Quoting now follows each engine's own folding rules, verified across seven engine and role combinations.
- ▸Generated base views quote identifiers the way each engine actually needs. The generator carried its own rule — quote only names containing a non-word character — so "column 1" came out right while MixedCase went bare and Postgres downfolded it into a column that did not exist.
- ▸Seeds loaded through the staged-parquet bulk lane now land the same column names as the ordinary path. The bulk lane declared its staging table from the raw file header, so a CSV column Number of units arrived as "number of units" on Trino and number_of_units on Postgres. Every row count matched; only a check by column name caught it.
- ▸MySQL, MySQL 5.7 and MariaDB work in federated models again — the connection mapper still refused the whole family with DVT011 after the adapters had been restored.
- ▸A MySQL-family target parses when a source declares a database that differs from its schema, so two MySQL-wire connections exposing the same schema.table stay distinct in compiled SQL.
- ▸Decorative comment banners no longer break models flipped onto MySQL, MariaDB or MySQL 5.7, where -- without a following space is syntax rather than a comment and failed with Error 1064.
- ▸Oracle as the default target no longer breaks federated models: bucket-backed names like sftp_docker__files.inbox/sftp_products.csv rendered completely bare (DVT099), and cross-connection joins failed with DVT031.
- ▸Flipping onto Oracle no longer strands quoted lowercase identifiers on an engine that folds to upper case — ORA-00904 on models referencing "title" while the seed stored TITLE.
- ▸Seed columns whose CSV values are space-padded numbers no longer land as text on Oracle, where the column arrived VARCHAR2 and MIN() came back a string.
- ▸Oracle bulk loads work on macOS. sling hardcodes SQL*Loader's log to /dev/stdout and SQL*Loader appends .log, so every load died with SQL*Loader-522: lfiopn failed for file (/dev/stdout.log).
- ▸SQL Server no longer chokes on unaliased expressions in the live read lane, where select count(*) failed as [rc=11] while the aliased spelling answered instantly — SQL Server hands the driver an empty column name for any unaliased expression, and the derived-table wrap the lane puts around every statement is illegal without one.
- ▸A model DVT federates because the target cannot express its SQL is no longer rewritten back into the target's dialect afterwards — on SQL Server that produced T-SQL brackets under an f_incremental config.
- ▸Running dvt commands concurrently no longer fails with a metadata-store lock refusal: the store retried 8 times over under two seconds, less than three writers need for open plus schema DDL, and now waits up to 30.
- ▸JSONL seeds load on Trino/Iceberg. A JSON-typed column reached the target unpinned, sling guessed json, and Iceberg refused it with Type not supported for Iceberg: json.
- ▸Large seeds to Trino/Iceberg stage a parquet file and commit once instead of arriving as thousands of INSERTs — declare a staging_connection to turn the lane on.
- ▸Tests on sources declared on a non-default connection no longer stop at a loud skip. The engine phase still excludes what it cannot reach, and those tests then run through DVT's virtualization lane: the test node is presented as f_table, its compiled SQL is decomposed, and returned rows are failures — 0 rows passes, severity: warn warns.
- ▸Federated tests honour federation_direct. The lane reads the chain — the test's own config, then dvt_project.yml, then the default true — so direct: false forces computation in DVT's engine instead of the single-hop shortcut overruling a config you wrote explicitly.
- ▸Writes to bucket and SFTP targets no longer fail at random with credential errors, caused by DVT handing sling both a named connection and a scheme-qualified object path that resolved into two competing targets.
- ▸dvt metadata refresh works against StarRocks, which had no interrogation query at all.
- ▸dvt generate-base-views exits 1 instead of 0 when you passed --select-table or --select-connection and the run ends up writing no files. It used to print a helpful note and exit 0, so a script that named a table and got a zero exit was told the job was done.
- ▸dvt seed --select a b takes every name you give it, not only the first.
- ▸Rebuilding an incremental after a flip no longer fails with DVT026 on SQL that dvt flip-target-to itself had written one hop earlier.
- ▸dvt flip-target-to --agentic onto a DuckDB or SQLite target no longer hangs forever on the database file's single-writer lock: the in-process re-parse held it while the engine judge spawned a separate process to EXPLAIN each rewritten model against the same file.
- ▸dvt sync no longer aborts on profiles containing sftp or filesystem connections.
- ▸DVT runs the transformation engine in its own Python instead of shelling out to whatever interpreter the dbt console script points at — a copied virtualenv could die with "Could not find adapter".
- ▸dvt flip-target-to --agentic no longer loses correct translations to Jinja-fidelity rejections on the blocks it masks: every {{ ... }} expression and {# ... #} comment becomes an opaque token the model cannot legally edit, and the validator compares token sequences instead of whitespace. It also can no longer write a model's explanation into the model file — one Oracle translation was saved with prose about ORA-00905 above the real SQL, and every validator passed it.
- ▸When flip-target-to's repair pass rewrites a model and the engine still rejects it, your original SQL is restored and the run says so per model. If a flip rolls back, files written more than once are restored to the true original rather than to the translated version.
- ▸flip-target-to no longer reports models as broken SQL when their upstream simply is not built on the new target yet — a flip never builds, so every ref()-reading model failed the compile check.
- ▸config-checkup's --select and --exclude match model names as well as file paths.
ADDED
- ▸dvt retract takes run's flags and run's selection language: --select/-s and --exclude (both repeatable), --models/-m, and the full graph grammar — +dim_packs, bronze_packs+, 1+dim_packs, @orders, tag:gold, path:. It prompts before dropping, and -y/--yes runs it headless. Retract works like run, backwards.
- ▸dvt retract removes what DVT wrote to SFTP servers. SFTP was the one connection type teardown skipped outright; it now has a real deletion lane authenticated from the same connection details the write path uses. Retract drops everything DVT created, on every engine and every bucket, with no exceptions left.
- ▸f_incremental models on Trino, SQL Server, Oracle and ClickHouse can use the keyless watermark pattern — a watermark_column plus where col > (select max(col) from {{ this }}) with no unique_key. Each engine joined only after its own hop measured it live. StarRocks, Snowflake, Databricks, BigQuery, Fabric and Athena still refuse with DVT026, deliberately.
- ▸On dvt flip-target-to --agentic, with an AI provider configured, a model that neither the deterministic transpiler nor the agentic conversation can express on the new engine is federated instead of stranded: incremental becomes f_incremental, table becomes f_table, the body is kept as or transpiled to DuckDB SQL, and DVT's own engine computes the result and lands it on your target.
- ▸dvt sync provisions the client-side bulk-load accelerator for the engines that need one — bcp for SQL Server, Fabric and Synapse, SQL*Loader for Oracle. On macOS with Homebrew it installs them for you (for Oracle: Instant Client, the client libraries symlinked so the loader can start at all, and a sqlldr shim); on other platforms it names the exact package and carries on. A missing accelerator is a warning, never a failed sync — without one sling falls back to batched INSERTs and seeds crawl.
- ▸dvt config-checkup tells you when a base view on disk was written by an older DVT and no longer spells its columns the way generate-base-views would today, before the build trips over it.
- ▸Per-adapter incremental strategy documentation: every adapter page now states which incremental strategies that engine supports and which are substituted, because the differences are real and were previously only discoverable by hitting them.
CHANGED
- ▸Oracle seeds run through SQL*Loader's direct path: 1,046,572 rows in 125 seconds, down from 424 — a 3.4x speedup, from ~2,400 to ~8,375 rows per second.
- ▸dvt flip-target-to clickhouse rewrites incremental_strategy: merge to delete+insert and announces the swap, because ClickHouse's adapter refuses merge by name.
- ▸Flipping onto a MySQL 5.7 target rewrites any model whose .sql file carries a literal config(materialized='ephemeral') to a view before classification, naming each file it touched. dbt injects ephemeral models into dependents as CTEs and MySQL 5.7 has none, so ephemeral is inexpressible there.
- ▸Extractions from Snowflake, Databricks and BigQuery retry once after 20 seconds, so a cold warehouse or a recycled session no longer takes the whole build with it. Any partial parquet is removed before the retry.
- ▸dvt sync checks where your adapters actually load from, not just what is installed: a stray copy inside the environment is uninstalled, one arriving over PYTHONPATH is reported by name with the variable to clear.
- ▸dvt retract no longer prints the "Keeping N relation(s) … declares as sources" banner. Those relations were never candidates for the drop, so announcing them made a complete retract read like a partial one. They are still kept — silently.
- ▸If you are on sling 1.5.22 or earlier, upgrade. 1.5.23 ends a segfault class that killed three battery runs on Oracle and SQL Server.
Get it: pip install dvt-core==0.2.51 · wheels on PyPI for macOS (arm64 + x86_64) and Linux x86_64, Python 3.10–3.14 — the full platform matrix.