This guide outlines a highly robust, sandboxed process for pulling, merging, and resolving conflicts when syncing changes from Google's upstream dataform-co/dataform releases (such as 3.0.58) into the SQLAnvil codebase.
Run every command here from the root of the
SQLAnvil/sqlanvilengine checkout, not from this docs repo. Paths like./scripts/...refer to that repo.
Last synced: upstream 3.0.61 → main on 2026-07-02 (merge commit e12ee80f). This sync also
adopted the bzlmod migration (upstream #2187, issue #41): Bazel 5.4.0 → 7.3.2, new MODULE.bazel +
MODULE.bazel.lock, WORKSPACE trimmed to just build_bazel_rules_nodejs. All the df/_main workspace-name
references were adapted to sa (MODULE.bazel module(name), tools/ts_library.bzl module_name,
tools/ts_proto_library.bzl workspace_name fallback, protos/BUILD module_name, both webpack.config.js
aliases, cli/index_test_base.ts runfiles path). macOS gotchas fixed: (1) upstream's sed -i in
ts_proto_library.bzl broke native BSD sed → made portable (temp-file + mv); (2) bazel build //... needs
--config=macos-cpp (ccache bypass + abseil -Wno-deprecated-builtins) to compile the C++ protoc tool — the
TS deliverables (core/cli/packages/tests) build without it. Prior sync: 3.0.59 → main 2026-06-03 (dfeb1e5d);
DF_VERSION had been bumped to 3.0.60 without a code merge. The local dataform branch mirrors the upstream
release tag; each sync advances main's merge-base with upstream, so the next merge only sees new commits.
The #1 lesson: the Bazel build — not grep — is the source of truth for rename leftovers. Git auto-merges most upstream changes cleanly, but those auto-merged regions carry upstream
dataform./df//@dataform//__dataform_*tokens that produce no conflict markers yet still break the renamed fork.tsc(via Bazel) flags every one. Grep is unreliable here: macOS BSD grep silently no-ops on\bword\band on-rgiven an explicit file list. Finish every sync by building, fixing what tsc reports, and rebuilding — never trust a clean grep alone.
To ensure your primary local main branch remains 100% stable during the merge, always perform the merge inside a temporary sandbox branch before merging back into main.
upstream/main (Google)
│
├── (Tagged Release: e.g. 3.0.58)
▼
[1. Fetch tag over HTTPS]
│
▼
[2. Create sandbox branch]
`upstream-sync/dataform-3.0.58`
│
▼
[3. Execute Merge & Resolve Conflicts]
- Mechanical import renames (df/ → sa/)
- Proto packages (dataform → sqlanvil)
│
▼
[4. Run Bazel Verification]
`./scripts/docker-bazel test //...`
│
▼
[5. Merge back to main]
Ensure your upstream remote is configured using HTTPS (to unblock sandbox egress) and fetch all tags:
# 1. Update upstream URL to HTTPS
git remote set-url upstream https://github.com/dataform-co/dataform.git
# 2. Fetch latest releases & tags
git fetch upstream --tagsCheckout a fresh sandbox branch from your local stable main branch:
git checkout main
git checkout -b upstream-sync/3.0.58Attempt to merge the targeted release tag (e.g. 3.0.58) into the sandbox:
git merge 3.0.58Upstream adds security pins to the resolutions block in package.json via Dependabot PRs
that land on their main between tagged releases. Because we take upstream selectively rather
than merging every tag wholesale, these are easy to miss — a one-line change buried in a release
diff, with nothing about a normal sync drawing attention to it. They are not cosmetic: yarn.lock
is consumed by yarn_install in WORKSPACE, so whatever it resolves reaches the build.
./scripts/check_upstream_resolutions # vs upstream/main
./scripts/check_upstream_resolutions 3.0.70 # vs a specific tagExit 0 means we cover everything upstream pins. Exit 1 lists what to add. Pins we carry that upstream lacks are reported but never fail — we ship adapters (pg, mysql) they do not, so we legitimately pin things they never see.
If it reports missing pins, add them to package.json, then:
yarn install # regenerate yarn.lock; expect transitive packages to drop outRead the advisory behind each one rather than assuming it is only a version bump, and re-run the
verification in §4 — a resolution can force a major version on a transitive dep (e.g.
brace-expansion 1.x → 5.x), which yarn will warn about and which only the build can vindicate.
Found the hard way on 2026-09-19: the fork was three pins behind (
braces,brace-expansion,linkify-it), all three resolving versions with advisories. Upstream PR #2327 supplied only one of the three; the other two had been sitting upstream for longer. Hence a scripted check rather than an eyeball.
Since the Dataform
Conflict: Upstream adds new protobuf fields inside package dataform; whereas SQLAnvil uses package sqlanvil;.
- Resolution:
- Keep SQLAnvil's package declaration:
package sqlanvil;. - Copy the new fields added by Google (e.g.,
string jit_code = ...insideAssertionmessage) and insert them using SQLAnvil naming conventions.
- Keep SQLAnvil's package declaration:
Conflict: Upstream imports use df/, e.g.:
import { ActionBuilder } from "df/core/actions";SQLAnvil uses sa/:
import { ActionBuilder } from "sa/core/actions/base";- Resolution:
- Standardize all new/merged imports to use the
sa/prefix.
- Standardize all new/merged imports to use the
Conflict: Upstream TypeScript code references Google's generated proto namespace dataform.Assertion, while SQLAnvil uses sqlanvil.Assertion.
- Resolution:
- Globally replace the merged references to use
sqlanvil.instead ofdataform..
- Globally replace the merged references to use
Not a git conflict. Upstream code that auto-merges cleanly — new helper bodies, new test cases, files upstream rewrote wholesale — arrives carrying dataform.X, df/… imports, @dataform/*, or __dataform_current_file. No conflict markers, but it compile-breaks the fork.
- Resolution:
- After resolving the visible conflicts, build and rename every token
tscreports:./scripts/docker-bazel build //core/... //cli/... //protos/... --jobs=2 --local_ram_resources=2048 - When upstream rewrote a whole file's apparatus (e.g.
cli/vm/compile.tscaller-file machinery in 3.0.59), don't resolve hunk-by-hunk —git checkout --theirs <file>, then re-apply the rename. Piecemeal resolution leaves auto-merged code referencing variables only the upstream side defines (e.g.coreBundlePath,needsCallerFileShim).
- After resolving the visible conflicts, build and rename every token
- Caller-file global: the exposed sandbox global must stay
__sqlanvil_current_file(read bycore/utils.ts); rename upstream's__dataform_current_fileto it.__df_enter/__df_exit/__df_currentare internal helper names — fine to leave. dataform.jsonclean break: never reintroduce thehasDataformJson/global.dataformJsonhandling upstream adds — SQLAnvil reads onlyworkflow_settings.yaml.- Extracted helpers: when upstream moves logic into helpers (e.g.
executionSql.createTableTasks/Operation/Assertion), the rename must follow into the auto-merged helper bodies; prior SQLAnvil behavior (e.g.disabled-action handling) is usually preserved inside them — verify rather than re-add. - CLI install-path tests: upstream tests that
npm i @dataform/core@<ver>become@sqlanvil/core@<ver>(unpublished) — they compile but fail at runtime. Skip or adapt; don't let them block the sync. df_in generated SQL / test fixtures: watch for non-namespace leftovers likedf_osc_,_df_temp_,df_integration_test→ rename tosa_. Casing artifacts too (readsqlanvil…→readSqlanvil…).
Once all conflicts are resolved, run the full validation suite. Run these natively — pass
--config=macos-cpp, which supplies the clang action env the protobuf C++ compile needs.
# 1. Build everything affected — THIS is what catches reintroduced dataform tokens
bazel build //core/... //cli/... //protos/... --config=macos-cpp
# 2. Run core compiler tests (the authoritative signal; cli e2e suites are flaky)
bazel test //core/... //cli/... --config=macos-cpp
# 3. Run the newly updated integration tests (needs the local Docker DBs up)
PG_HOST=localhost PG_PORT=5432 bazel test //tests/integration:postgres.spec --config=macos-cpp \
--test_env=PG_HOST --test_env=PG_PORT --test_env=PG_USER --test_env=PG_PASSWORD --test_env=PG_DATABASEFallback: scripts/docker-bazel
Native macOS Bazel used to fail with a wrapped_clang / dyld LC_UUID toolchain error compiling
protobuf C++, and this guide routed everything through the container as a result. That is
resolved — native with --config=macos-cpp is the primary path as of 2026-09-19. The container
route is kept for when the native toolchain breaks again or you need to reproduce CI exactly.
Always pass --jobs=2 --local_ram_resources=2048: the in-container Bazel JVM gets OOM-killed
(Socket closed, error 14) under default parallelism during webpack bundling. Note the integration
tests need PG_HOST=host.docker.internal there rather than localhost.
./scripts/docker-bazel build //core/... //cli/... //protos/... --jobs=2 --local_ram_resources=2048
./scripts/docker-bazel test //core/... --jobs=2 --local_ram_resources=2048
PG_HOST=host.docker.internal PG_PORT=5432 ./scripts/docker-bazel test //tests/integration:postgres.spec \
--test_env=PG_HOST --test_env=PG_PORT --test_env=PG_USER --test_env=PG_PASSWORD --test_env=PG_DATABASEIf the build completes and all tests pass:
# 3. Checkout main & merge the verified sync branch
git checkout main
git merge upstream-sync/3.0.58
# 4. Clean up the sandbox branch
git branch -d upstream-sync/3.0.58
# 5. Push updated main to origin (GitHub)
git push origin mainBugs we fixed locally that are also present upstream and that we've reported to
dataform-co/dataform. On each sync, check whether upstream adopted the fix — if so, take
their version and drop our local change so the file stops diverging (less future merge friction).
If not, keep ours and re-apply over the merge.
| File | Local fix | Upstream issue / PR | Status |
|---|---|---|---|
common/flags/index.ts |
Lenient arg parser — ignore non-flag tokens instead of throwing Arg neither flag name nor flag value (which crashed the CLI when a positional followed a flag). Extracted parseArgvFlags() + test. |
issue dataform-co/dataform#2198 → fixed by PR #2199 | PR open, not merged (not on main, not tagged) — watch #2199 for the convergence trigger |