XRAPH/Research/Technical note

Dialect-Native Query Generation: Against the Portable Subset

Portable object-relational mappers express queries in the intersection of what their backends support, and applications escape that subset at the first real query. Measures the cost of eliminating query-time reflection on a single insert microbenchmark, and argues the module boundary belongs below query construction.

Type
Technical note
Year
2025
Status
Draft
Length
3 min read

#Abstract

Portable object-relational mappers express queries in the intersection of what their backends support, and applications escape that intersection at the first query with real requirements. This note argues the module boundary belongs below query construction rather than above it, reports the cost of eliminating reflection from the query path as measured on a single insert microbenchmark, and states the conditions under which the portable design is the correct one.

#The intersection problem

Each supported backend contributes constraints, and the expressible language is their intersection. Distinct-on, containment operators over semi-structured columns and skip-locked selection exist in some engines and not others. Column-oriented engines have pre-filtering and sampling constructs with no row-store equivalent. Document stores have aggregation pipelines that do not translate.

The observed consequence is that applications use the mapper for simple access and drop to raw queries for the rest, producing two access patterns where the mapper covers the half that needed the least help.

#Where the boundary belongs

Parnas asks which decision a module hides . Connection management, metadata caching, migration ordering, hooks and result streaming are genuinely common across backends, and a module hiding them serves its purpose. Query construction is not common, and a module claiming to hide it is hiding a decision that differs materially between engines.

The proposal is a shared core with a per-driver query builder reached by an explicit unwrap. Portability is surrendered deliberately and visibly, at a point in the code a reader can see.

Portability across engines is a property most applications never exercise and every portable mapper charges for.

#Measured cost of reflection

Insert benchmark, in-memory embedded engine, Go 1.25.7 on arm64, five runs averaged. Absolute values are less interesting than the ratios.

  • Raw driver interface: 4,015 ns per operation, 880 bytes, 20 allocations.
  • The described implementation: 4,381 ns, 1,283 bytes, 28 allocations. Approximately 9 per cent over raw.
  • Comparison mapper A: 8,459 ns, 5,470 bytes, 27 allocations. Approximately 111 per cent over raw.
  • Comparison mapper B: 10,265 ns, 4,954 bytes, 66 allocations. Approximately 156 per cent over raw.

The mechanism is that struct metadata is resolved once at registration and cached, so the query path walks a prepared description rather than the reflection API, with pooled buffers. This is available to any mapper willing to give up runtime schema flexibility, and the trade is stated rather than claimed as a free improvement.

#Interpretation of the numbers

This is one workload against an in-memory engine, which maximises the proportion of time spent in the driver. Across a network round trip to a server, all four figures are within noise of each other, and any claim of a general performance advantage would be unsupported.

Where it matters is bulk insert paths and write paths that multiply per-command overhead by a fan-out factor, and those are the cases the design targets.

#Adoption

An unexpected finding. The feature that most affected adoption was reading a competing mapper's struct tags as a fallback, allowing an existing codebase to adopt the library without modifying its models. This took an afternoon and outperformed several features that took weeks.

The generalisable point is that the cost of the first step dominates adoption decisions, and reducing it is often cheaper than improving the destination .

#When the portable design is correct

Where a team wants one query language and is indifferent to the engine underneath, the portable mapper serves that preference well and this design does not. The argument here applies where an engine was chosen deliberately for properties the application depends on, including its isolation behaviour and its execution model .

#Limitations

A single microbenchmark on a single workload. No application-level benchmark is reported, and an application-level comparison is what would establish whether the difference is ever material outside bulk paths.

No measurement of the maintenance cost of per-driver builders is offered, and that cost is real: each engine's builder is code that must be tested against that engine.

References

  1. [1]D. L. Parnas, On the Criteria To Be Used in Decomposing Systems into Modules, Communications of the ACM, vol. 15, no. 12, pp. 1053-1058, 1972doi:10.1145/361598.361623
  2. [2]Hal Berenson et al., A Critique of ANSI SQL Isolation Levels, ACM SIGMOD International Conference on Management of Data, 1995doi:10.1145/223784.223785
  3. [3]Martin Kleppmann, Designing Data-Intensive Applications, O'Reilly Media, 2017
  4. [4]Frederick P. Brooks Jr., No Silver Bullet: Essence and Accidents of Software Engineering, Computer, vol. 20, no. 4, pp. 10-19, 1987doi:10.1109/MC.1987.1663532