Skip to content

Add the DuckDB execution engine - #25

Open
dionmcm wants to merge 1 commit into
upstream/engine-seamfrom
upstream/duckdb-engine
Open

Add the DuckDB execution engine#25
dionmcm wants to merge 1 commit into
upstream/engine-seamfrom
upstream/duckdb-engine

Conversation

@dionmcm

@dionmcm dionmcm commented Aug 29, 2026

Copy link
Copy Markdown
Member

The engine, selected with rvf.execution.engine=duckdb against the seam in #24. No MySQL, no JPA, no datasource: a validation runs against a DuckDB file database built from the release under test, with assertion SQL from a precompiled store.

Review order: #23, #24, then this.

Classes

class role
DuckStore / DuckStoreLocator read the store; refuse one not published from the corpus in this artefact
DuckBinder sentinel substitution mirroring MySqlQueryTransformer, including the per-statement drop for unbound <PREVIOUS>/<DEPENDENCY>
DuckReleaseUnpacker / DuckMaterialiser unpack an RF2 archive, materialise files as tables
DuckDbAssertionExecutionService prepareSchema() then execute(assertions)
DuckDbValidationService the SqlAssertionValidationService implementation, and the resource/assertion phase ordering the MySQL service also needs
DuckAssertionService / DuckAssertionSource supply the AssertionService interface from the store, which keeps the submission REST surface working without assertion tables
DuckFailuresExtractor whitelist and module filtering from qa_result

Why the store is precompiled and checked in

Transpiling MySQL to DuckDB is the only part that needs Python. Doing it at runtime would put a Python toolchain and a sqlglot version inside the server. Precompiling makes the dialect a diffable, version-stamped artefact. duck/README.md has the republish procedure.

duckdb_jdbc is pinned to 1.5.5.1 to match the sqlglot dialect the store was transpiled against.

BundledStoreMatchesCorpusTest

A store published from a different corpus than the one shipped beside it would execute old SQL and report it under new assertion text and uuids — a complete, plausible, wrong report.

It fired during this work. The store came from corpus 0160dd2; develop's corpus had moved one commit to 694b06d (VAL-440), rewriting 20 release-type scripts. The store here is republished from 694b06d; only the assertions section changed, ports and prerequisites are byte-identical.

Consequence for you: checkout-resources.sh clones the corpus unpinned (--branch develop) and the pom runs it during the build, so this store needs republishing whenever the corpus moves. Pinning that clone is a separate question.

One measured defect

DuckDB sizes its thread pool from the machine and ignores CPU affinity and cgroup quotas. On a 10-core host under taskset -c 0-1, nproc and the JVM report 2; DuckDB reports 10. A pod with limits.cpu: 2 on a 64-core node would run 64 threads inside a two-core quota. Bounding it is worth 2.6x at one core (674s to 259s on a full edition).

rvf.duck.threads therefore defaults to Runtime.availableProcessors(), overridable for benchmarking. rvf.duck.memory.limit is unset by default.

Verification

clean upstream-develop   213 run, 0 failures, 142 errors, 21 skipped
+ #23                    219 run, 0 failures, 142 errors, 21 skipped
+ #24                    241 run, 0 failures, 142 errors, 21 skipped
+ this PR                328 run, 0 failures, 142 errors, 21 skipped

87 tests added; the same 142 pre-existing errors (Testcontainers, no Docker daemon).

Also run end to end on a real 894MB edition through the REST API with no MySQL present: POST /run-post -> 201, worker consumes, GET /result/{runId} -> 191 tests run, 58 failures, 1 warning, 0 skips, 54 incomplete, in 194s. The 54 incomplete are the expected result of supplying no previous or dependency release.

Selected with rvf.execution.engine=duckdb against the seam in the previous
commit. No MySQL, no JPA, no datasource: a validation runs against a DuckDB file
database built from the release under test, and the assertion SQL comes from a
precompiled store rather than from the assertion tables.

The engine, in org.ihtsdo.rvf.core.service.duck:

* DuckStore / DuckStoreLocator - read the precompiled store, and refuse a store
  that was not published from the corpus in this artefact.
* DuckBinder - sentinel substitution, mirroring MySqlQueryTransformer, including
  its rule that a statement still holding an unbound <PREVIOUS>/<DEPENDENCY> is
  dropped per statement rather than per assertion.
* DuckReleaseUnpacker / DuckMaterialiser - unpack an RF2 archive and materialise
  its files as tables.
* DuckDbAssertionExecutionService - prepareSchema() then execute(assertions).
* DuckDbValidationService - the SqlAssertionValidationService implementation, and
  the resource/assertion phase ordering the MySQL service also relies on.
* DuckAssertionService / DuckAssertionSource - supply the AssertionService
  interface from the store, which is what keeps the submission REST surface
  working without the assertion tables.
* DuckFailuresExtractor - the whitelist and module filtering, from qa_result.

Why the store is precompiled and checked in rather than generated at build time:
transpiling MySQL to DuckDB is the only part of this that needs Python, and
doing it at runtime would put a Python toolchain and a sqlglot version inside the
server. Precompiling makes the dialect a reviewable, diffable, version-stamped
artefact instead of something regenerated invisibly on every run.

BundledStoreMatchesCorpusTest is what stops that becoming a silent trap, and it
earned its place during this carve: the store shipped here was published from
corpus 0160dd2, and IHTSDO's develop had moved one commit ahead to 694b06d
(VAL-440), which rewrote exactly 20 release-type scripts. The test failed with
'20 of 360 assertions differ' rather than letting a run execute the old SQL and
report it under the new assertion text. The store in this commit is republished
from 694b06d, and only the assertions section changed - ports and prerequisites
are byte-identical.

duckdb_jdbc is pinned to 1.5.5.1 to match the sqlglot dialect the store was
transpiled against; a different minor can parse the same SQL differently.

rvf.duck.threads defaults to Runtime.availableProcessors() because DuckDB sizes
its own pool from the machine and ignores affinity and cgroup quotas - measured
here, under taskset -c 0-1 on a 10-core host DuckDB reports threads=10 while the
JVM correctly reports 2, and a container CPU limit is worse: 64 threads inside a
two-core quota. Bounding it correctly is worth 2.6x at one core.

87 tests added, including a probe harness that runs a real release end to end.
Full suite: 328 run, 0 failures, 142 errors, 21 skipped - the same 142
pre-existing Testcontainers-without-Docker errors as clean develop.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant