Protocol v1 - Turn Ashurbanipal into a protocol: spec, conformance kit, and two new ports#5
Open
mt-empty wants to merge 16 commits into
Open
Protocol v1 - Turn Ashurbanipal into a protocol: spec, conformance kit, and two new ports#5mt-empty wants to merge 16 commits into
mt-empty wants to merge 16 commits into
Conversation
Derive the API base from location.pathname instead of hardcoding /__ashurbanipal/api, so the UI works behind any reverse-proxy prefix. Demo gains an optional MOUNT_PREFIX env var to simulate one; covered by a new e2e spec and a black-box regression guard against the literal returning.
spec/protocol.md and spec/openapi.yaml become the cross-implementation contract (docs/design.md §4 stays as rationale); spec/filter-dsl.md moves under spec/, rescoped to the frontend's grammar. The filter query param's wire format changes from DSL text to a JSON AST - dbviewer.html gains the grammar parser and parses client-side at submit; src/filter.rs drops its tokenizer for AST deserialization + structural validation. Backed by shared fixtures (spec/fixtures/parser-tests.json, filter-builder-tests.json) consumed by both the Rust suite and a new Playwright spec. Also hardens the reference to match the spec: statement timeout on every query (previously only table-data queries were bounded), current_schema() instead of hardcoded 'public', an open enabled_for vocabulary (production-like names still rejected at config load), and an x-ashurbanipal-protocol response header on every API route.
Repurposes tests/black_box/ into conformance/runner/, adding an external-URL mode (ASHURBANIPAL_CONFORMANCE_URL) so the same suite can target a port instead of only the spawned reference demo. Backed by a new conformance/seed/ (extends tools/seed-gen with composite-FK, no-ANALYZE, and cross-schema fixtures) and a coverage matrix mapping every MUST in spec/protocol.md to a test, with honest gaps documented rather than silently skipped. Assertions go through four explicit comparison tiers instead of ad hoc equality checks - exact match, type/range only (total_approx, common-values frequencies), status-code only (error responses, since body text is implementation-defined), or not checked at all - so a reviewer doesn't have to reclassify each assertion by eye. docs/design.md is replaced with the design-alt.md draft: it's what the project has actually been following since the three-layer conformance model (schema generation, schema fuzzing, behavioral fixtures) was worked out, so design-alt.md's content becomes the real design doc rather than living as a parallel draft.
The Rust crate moves to implementations/rust/ so it's structurally a peer, not a reference other implementations port from - Spring Boot, Go, and Elixir land as siblings under implementations/ later, not ports/. Pulls Phase 3's frontend move forward too: dbviewer.html was already going to be released as a shared artifact, and it can't stay nested inside one implementation's source tree once no implementation is privileged, so it moves to a repo-root frontend/ now instead. spec/openapi.yaml goes back to hand-maintained (utoipa is dropped) - layer 1 of the conformance model stops being "structurally can't drift" and becomes governance: one PR touches spec/protocol.md + spec/openapi.yaml + fixtures together, and drift between the schema and actual behavior is caught after the fact by layer 2's schema fuzzing rather than prevented by construction. That tradeoff is stated explicitly in docs/design.md rather than papered over. Also fixes a table-listing e2e test that's been broken since the conformance kit landed (85a7eb7 added four tables to the seed without updating this fixture's hardcoded list), caught while re-running the full suite after the move.
Adds the protocol-version stamp to dbviewer.html, a NOTICE for implementations that vendor it outside a repo clone, a PORTING.md stub covering just the vendoring contract (pin a tag, record the sha256, re-verify it in your own CI), and a release workflow that attaches the frontend, LICENSE, and its checksum to a GitHub Release on tag push. crates.io publishing stays deliberately out of this workflow - it's a one-way action needing a registry token, not something to bundle in by default.
loadData()'s resetScroll path only zeroed #main's vertical offset; switching to a narrower table could leave the viewport scrolled right with nothing to show.
New conformance/runner/schema-check.sh fires schemathesis at a running implementation - spawned by default, or ASHURBANIPAL_CONFORMANCE_URL to target an external one, mirroring the layer-3 runner's split. Wired into a separate CI job so shape and behavior failures never interleave, plus mise tasks for local runs. Fuzzing surfaced real drift between spec/openapi.yaml and actual behavior: `limit` rejected out-of-range values with 400 instead of clamping them, violating protocol.md's explicit MUST. Fixed both directions - the deserializer now saturates negative and overflowing values before the handler's own clamp() runs, not just the overflow case initially caught. The root cause was in the schema itself: openapi.yaml declared `minimum: 0` on `limit`, which is a JSON Schema rejection constraint - self-contradictory for a field whose contract is "never rejected, only clamped". Removed it; a schema can't accurately describe a field it also mis-declares. Two `positive_data_acceptance` cases stay excluded, documented rather than suppressed: identifier validity (empty/unknown table or column) is a runtime information_schema lookup no static schema can express, and the filter AST's positional logic rule (absent on element 0, required after) isn't expressible via JSON Schema's uniform `items`.
offset had the identical bug limit did: a negative or overflowing value 400'd instead of clamping, and spec/openapi.yaml declared the same false minimum: 0 rejection constraint. Unlike limit's fix this wasn't caught by schemathesis - /tables/data's own params weren't re-fuzzed after the limit fix - but the two fields go through the same deserializer and the same reasoning applies: a negative offset has an obvious clamp target (0, same as omitting it), and an oversized one already degrades harmlessly today (SQL OFFSET past the table just returns nothing, per the existing offset_is_unclamped_and_beyond_table_ size_returns_empty_rows test). protocol.md was previously silent on offset's out-of-range behavior (unlike limit's explicit "never an error") - added the same MUST so the spec and implementation don't just happen to agree by coincidence.
implementations/spring-boot-starter/: an @autoConfiguration starter that mirrors the Rust reference's fail-closed kill switch, filter AST validation, and catalog queries (composite-FK omission, current_schema() scoping, ::text-cast serialization). No Docker available here, so integration tests hit the same live $DATABASE_URL every other suite in this repo already uses instead of Testcontainers. Both conformance layers pass against a live instance, independently re-verified rather than just accepting the build log: the golden-fixture runner (40/40) and schemathesis (346/346) produce identical numbers to the Rust reference's own runs against the same spec. Writing a second implementation surfaced a real spec gap: protocol.md was silent on *where* the ::text cast must happen, which matters because a JVM decode-then-toString() step can silently diverge from Postgres's own cast (locale, timezone) while still technically returning a string. Added the MUST. PORTING.md gains the CSP/inline-script section the hardening checklist called for - dbviewer.html's inline <script> breaks under a strict CSP identically for every port, not just this one. No publishing wired up (no Maven Central credentials, matching the same stance already taken on crates.io) - the workflow builds and re-verifies the vendored frontend's hash on every run but never ships anywhere.
… 9.6.1 - Replace io.spring.dependency-management with Gradle-native platform() BOM import - Migrate Jackson 2 -> Jackson 3 (tools.jackson.*), Spring Boot 4's new default - Fix JsonNode.map member/extension shadowing at call sites (cast to Iterable<JsonNode>) - Explicitly enable FAIL_ON_UNKNOWN_PROPERTIES (no longer Jackson 3's default) - Add <T : Any> bound to apiOk for Kotlin 2.2 + Spring Framework 7 null-safety - Replace deprecated 'by tasks.registering' with tasks.register (Gradle 9.6+)
Group tasks under rust:*, spring:*, frontend:*, conformance:* with umbrella tasks (mise run rust/spring/frontend/conformance) and a top-level check depending on all four. Adds new spring:build/test/check tasks for implementations/spring-boot-starter (previously had none). Updates all cross-references to renamed task names in CI, docs, and e2e test comments.
Extract the 'start demo, run conformance kit, run schemathesis' steps duplicated across implementations into two workflow_call templates: _conformance-behavior.yml (layer 3) and _conformance-schema.yml (layer 2), kept as separate jobs so a shape failure and a behavior failure never interleave in one log. conformance.yml (Rust) is replaced by conformance-rust.yml, which calls both templates with Rust-specific inputs. spring-boot-conformance.yml keeps its own gradle build-and-test job but now calls the same two templates for its conformance/schema-conformance jobs instead of duplicating the steps inline. Future ports (Go, etc.) add a thin per-implementation trigger file that calls the same templates rather than copy-pasting the whole sequence. Updates readme.md/PORTING.md references to the renamed workflow file.
A net/http-native library at implementations/go-nethttp: func Router(cfg
Config, db *sql.DB) (http.Handler, error) mounts the six routes (UI +
five API) into any net/http-compatible mux, with no framework choice
baked in. Fail-closed via the error return on a production-like
EnabledFor (Config{} zero value is disabled by construction, matching
the Rust and Kotlin ports); catalog SQL ported line-for-line against
db.rs (information_schema/pg_stats queries, composite-FK omission,
current_schema() scoping, ::text-cast-in-SQL for value serialization);
filter AST validation and WHERE-clause building ported against
filter.rs, with a defense-in-depth operator re-check in
BuildWhereClause since Go's plain string Condition.Op has no
type-level guarantee the way Rust's FilterOp enum does. Siblings
health fan-out via errgroup + per-check context timeout. Vendored
frontend/dbviewer.html re-hashed against the pinned sha256 on every
package load (embed.go), not just recorded once.
Tests: filter_fixture_test.go runs all 30 cases from
spec/fixtures/filter-builder-tests.json directly; killswitch_test.go
covers the fail-closed contract (zero-value disabled, production-like
EnabledFor errors, production-like running environment silently
disables); integration_test.go boots a real httptest.Server against
the live seeded DATABASE_URL. 65/65 tests green under -race.
Verified via cmd/demo against both conformance layers: 40/40 behavior
(conformance/runner/report.sh) and 346/346 schema
(schema-check.sh), matching the Rust reference's and Spring Boot
starter's own numbers.
Extends the _conformance-behavior.yml/_conformance-schema.yml reusable
templates with a go toolchain branch (actions/setup-go), and adds
go-conformance.yml as the port's own CI trigger, mirroring
conformance-rust.yml/spring-boot-conformance.yml. Adds the registry
row to readme.md.
PORTING.md grows from a vendoring-only draft into the full guide: what a port is, what it reuses, what it implements (keyed to spec/protocol.md section numbers), what it MUST NOT do, how to run both conformance layers locally and in CI, the listing bar, and a Governance section (one-PR rule, protocol versioning, registry staleness) housing the Phase 5.5 cross-port hardening checklist where it was always meant to live. Sidecar guidance is deliberately left out of this pass. The checklist's sign-off log records an actual read-through of all three implementations, not just a template: confirmed cast-in-SQL, fail-closed defaults, parameterization, and catalog-SQL parity for Rust, Spring Boot, and Go, and read each port's kill-switch test directly rather than trusting green CI. That review surfaced one real gap in the reference itself — no test covered `enabled_for` being entirely absent from the TOML text (only the constructed-empty-list case) — closed here with config::tests::disabled_when_enabled_for_absent_from_config. Adds spec/CHANGELOG.md, seeded with v1 as the initial version per the governance section's versioning policy.
…query strings ci.yml's check job ran `mise run check` (which fans out to conformance:schema-test) without ever installing schemathesis, so it failed immediately with "schemathesis not installed" — the reusable _conformance-schema.yml workflow had its own install step but ci.yml never got one. Add `mise run conformance:schema-install` before it. Separately, schemathesis fuzzing found the Spring Boot demo returns 500 for GET /tables/data and GET /tables/common-values when the query string contains a parameter with an empty name (e.g. `?=true&table=x`). Tomcat's Parameters.processParameters throws InvalidParameterException (an IllegalStateException) while Spring resolves @RequestParam, before any handler code runs; it fell through to the generic 500 handler. Added a specific IllegalStateException handler mapping to 400, since a query string the container can't even parse is a client error, not a server fault. Verified against the seed that found the failures: schemathesis now reports 0 issues over 746 generated cases.
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.
Ashurbanipal started as a single Rust/Axum crate. This branch inverts
that: the frontend and a normative protocol are now the product, and
every backend — including the original Rust crate — is a peer
implementation conforming to them, none structurally privileged.
spec/protocol.md+spec/openapi.yaml— the normative endpointcontract (transport, mount contract, kill switch, the five routes,
server invariants, protocol versioning), extracted from
docs/design.mdand made authoritative.
spec/CHANGELOG.mdtracks version historygoing forward.
(
spec/filter-dsl.md) is now a frontend-only concern; every backend'sobligation is defined purely against the JSON filter AST
(
spec/fixtures/filter-builder-tests.json), shrinking each port'sfilter code to validation + WHERE-clause building — no tokenizer, no
grammar, no quoting rules to replicate per language.
conformance/): a golden-fixturebehavior runner (
conformance/runner, HTTP-only, works against anyrunning instance via
ASHURBANIPAL_CONFORMANCE_URL) and schemathesisshape-fuzzing against
spec/openapi.yaml— both wired into reusableCI templates (
_conformance-behavior.yml,_conformance-schema.yml)every implementation's workflow calls into.
mount-point agnostic (derives its API base from
location.pathname),released with a sha256 and
NOTICEon tag, so a port vendors it ratherthan forking it.
the next started: the Spring Boot starter (Kotlin autoconfiguration
module) and the Go
net/httpport (framework-agnostic library).Both pass both conformance layers and carry their own fail-closed
kill-switch tests.
PORTING.mdis now a complete guide — what a port is, what itreuses, what it implements (keyed to spec section numbers), what it
must not do, how to run conformance locally and in CI, the listing
bar, and a governance section (one-PR-touches-spec-and-fixtures-and-
implementations rule, protocol versioning policy, registry staleness)
— plus a seven-item cross-port hardening checklist covering the
properties neither conformance layer can observe (cast-in-SQL
location, fail-closed-on-absent-config, injection-absence, vendoring
integrity, kill-switch self-certification, catalog-SQL parity), signed
off against all three implementations.
Boot starter, and Go port are three equal rows in
readme.md'sregistry, each with its own conformance CI link.
Explicitly out of scope for this branch (recorded as deliberate, not
missing): sidecar guidance for unsupported stacks (pgweb recommendation),
a third port (Elixir/Phoenix — cancelled, not deferred), and actually
cutting the first tagged release.
Test plan
mise run check(or the Rust/Spring/Go/frontend/conformanceumbrella tasks individually) green
conformance/runner/report.shandconformance/runner/schema-check.shpass against all three implementations' demos
mise run rust:demoagainst the seeded devcontainer db