vernac / architecture, expressed.
Browse documentation

Declarative repository queries

Status

Implemented in the project compiler, JDBC runtime and LSP. PostgreSQL integration tests are opt-in and must be run against the supported PostgreSQL deployment. Existing byId, save and delete remain available. Custom JDBC searches, pagination and query projections remain deferred.

Declaration and result contract

aggregate Tour[TourId](Title title, TourStatus status,
                      DispatchReference? reference) list Tours;

repository TourRepository for Tour {
    find Tours matchingStatus(TourStatus status)
        where status = :status
        order by title asc;

    find Tour? matchingReference(DispatchReference reference)
        where reference = :reference;

    find Tours createdBefore(Instant cutoff)
        where createdAt < :cutoff
        order by createdAt asc;

    find Tours withoutReference()
        where reference is absent
        order by updatedAt desc;
}

The snippet assumes the referenced domain types have been declared. Method names are chosen by the developer and never parsed to infer predicates. Fields on the left refer to the aggregate model; :name refers to a declared method parameter. Logical id, createdAt and updatedAt refer to root identity and metadata; physical SQL names never appear in query declarations. Technical persistence version is not a query field in this increment.

Initial expression scope

Explicit =, !=, <, <=, > and >= comparisons combined with and, or, not and parentheses; is absent / is present for optional root fields supported by this increment. Range predicates apply to the types with ordering support in the table below, including root createdAt and updatedAt (Instant). No implicit conversion between distinct Vernac types, even if their scalar representations match. A non-optional parameter may compare to an optional field; absence does not match that parameter, for either = or !=. Optional query parameters are deferred.

Value predicates address root IDs, enums and single-value objects directly wrapping one required supported scalar. Multi-value equality, nested field paths, nested VO wrappers, entity traversal and collection predicates are deferred with explicit compiler diagnostics. Arbitrary Java types, expressions, SQL and projections are not accepted. is absent refers to model presence, not to an arbitrary nullable storage column; any later extension must reuse the storage plan’s presence rules.

In 0.4.0, a root may declare several collections. A query selects its return type explicitly; the resolver checks that particular collection’s element identity and list/set kind, rather than treating the first declaration as a default.

Two kinds of order

A persisted contained list has owner-local positions. Loading that list orders by its stored position. These positions do not define an order between aggregate roots returned by an unrelated search.

Query List result Set result
Explicit order by Preserve declared order Compiler error
No order by Compiler warning; order unspecified Allowed; order unspecified

Never append an implicit UUID, timestamp or other tie-breaker. Equal sort keys leave relative order unspecified. The presence of order by means a declared ordering, not a proven total order; no uniqueness proof is required in this increment. For optional sort values, absence is always last, for ascending and descending order. Direction defaults to ascending. A singleton result does not accept ordering: it must diagnose multiple matches rather than hide them by choosing the first.

No pagination API is introduced yet. A later limit must count aggregate roots, not joined entity rows, and address ties explicitly.

Equality contract

For supported values in the persistence mapping’s supported domain, a query matches if and only if the reconstructed field value equals the parameter according to the generated Vernac/Java equals implementation. Both false positives and false negatives violate the contract. Use the same resolved types, column components, enum codes and scalar codecs as persistence; do not reconstruct mappings from names.

Current ValueObjectGenerator uses Objects.equals for all fields, including boxed primitive arguments. Consequently floating-point equality is wrapper equality, not primitive ==. BigDecimal equality includes scale. A comparison must cover all identity-relevant components, not only the main column.

Scalar capability table

This table concerns the scalar inside a single-value object. Primitive and boxed forms share the policy. Ordering is deliberately a narrower capability than storage. = describes equality; != negates that comparison for present values. Range comparisons use the same order as order by.

Scalar = implementation order by and range comparisons
boolean / Boolean Boolean equality Deferred
byte, short, int, long and wrappers Exact integral equality Numeric
char / Character Stored UTF-16 code unit equality Numeric code unit
String Exact stored text, deterministic C collation Explicit PostgreSQL C collation
UUID / Vernac ID UUID equality Deferred; no implicit ID ordering
BigInteger Exact integral NUMERIC equality Numeric
BigDecimal Numeric equality AND stored scale equality Numeric value; equal magnitudes may tie despite different scales
Currency Exact currency code Deferred
Year Year component Numeric year
Month / DayOfWeek Stored numeric component Deferred
ZoneId Exact zone identifier, no alias normalization Deferred
ZoneOffset Offset seconds Deferred
LocalDate Stored date Calendar order
LocalTime Microsecond time AND nanosecond remainder Lexicographic time and remainder
LocalDateTime Microsecond timestamp AND nanosecond remainder Lexicographic timestamp and remainder
Instant Microsecond instant AND nanosecond remainder Chronological, including remainder
OffsetDateTime Microsecond instant AND remainder AND offset seconds Deferred
ZonedDateTime Local timestamp AND remainder AND offset AND exact zone ID Deferred
OffsetTime Local time AND remainder AND offset seconds Deferred
Duration Seconds AND nanos Lexicographic seconds and nanos
Period Years AND months AND days, without normalization Deferred
YearMonth Year AND month Lexicographic year and month
MonthDay Month AND day Lexicographic month and day
float / Float, double / Double Java wrapper equality: binary floating representation, with all NaNs treated as equal Deferred
Vernac enum Equality using the persisted code mapping Deferred

String sorting under C is a defined technical order, not linguistic sorting and not a promise of Java String.compareTo order for all Unicode values. Equality performs no case folding, whitespace trimming or Unicode normalization. Textual components of composite scalars and enum codes require the same exact equality. These rules do not expand PostgreSQL TEXT’s supported character repertoire.

Floating-point equality uses float4send / float8send to distinguish signed zero and explicitly matches NaN against NaN, independent of its payload. Float ordering and range comparisons are deferred. This is not an approximate comparison. See the scalar mapping audit and storage limits. Query parameters use the same codec and representation checks as writes.

For metadata Instant range comparisons and ordering, compare the complete tuple (microsecond timestamp, nano remainder). A predicate that only compares the first column loses ordering within a microsecond. Apply sort direction to both components.

Compiler and tooling requirements

One resolved query representation must drive semantic checks, SQL generation and LSP diagnostics. Do not parse query method names or resolve types again in generators. Check result kind, aggregate identity, field existence, parameter existence/type, method-signature collisions, operator support and ordering support before generation. Unsupported syntax or mappings must produce source-located Vernac diagnostics.

The editor must report list-without-order warnings and set-with-order errors, complete fields/parameters where appropriate and navigate to their declarations. Generated Java must have the agreed Optional/non-null API and existing NullMarked policy. Parameter values are bound, never concatenated into SQL. Sort identifiers come exclusively from resolved model bindings.

Implementation acceptance tests

Persistence integration tests must execute on PostgreSQL. String assertions on rendered SQL alone cannot establish the equality contract. Document the supported subset, examples, diagnostic remedies and ordering guarantees before release.

References and next step

Implementation inspected: ScalarMappings, ScalarCodec, StoragePlan and ValueObjectGenerator at baseline commit 96212bb.

The implementation acceptance tests include real generated Java compilation, PostgreSQL equality/inequality across scalar families, selected range comparisons, complete graph loading and interleaved update/delete detection. Run them as shown in the repository query tutorial.

Result membership is determined by the initial root SELECT. At READ COMMITTED, concurrent inserts can be absent from this result; a change/delete of a selected root detected during graph loading raises an optimistic-lock exception. There is no promise of a global serializable snapshot. Root versions are checked after loading the relations. Readers do not lock every matching root.

Relations are fetched in batches of up to 1000 aggregate IDs per table. Each root has its own entity identity map, including when different roots contain equal entity IDs. Optional singleton searches select at most two roots and reject multiple matches before graph reconstruction.

Parameters must have explicit, unique, non-optional names and must be used. Names beginning with __vernac are reserved for generated query locals. Generated helper method names are also rejected. Java generics are not repository result syntax. find Tours all() order by title; explicitly selects every root; no WHERE clause is required when no restriction is intended.

The editor completes root fields and parameters and navigates their references. Metadata fields navigate to the aggregate declaration. SQL identifiers and raw SQL are never accepted as query expressions.

Text searches are supported as specified below. Alternative locale-aware sort orders remain deferred.

Text searches

find Tours matching(String pattern) where title like :pattern order by title;
find Tours containing(String text) where title contains :text order by title;
find Tours beginning(String text) where title starts with :text order by title;
find Tours ending(String text) where title ends with :text order by title;

These operators require a String-backed single-value field and a required String parameter. A fragment or pattern need not satisfy the field’s domain invariants. Equality and inequality still require the exact domain type.

contains, starts with and ends with treat percent signs, underscores and backslashes literally. like matches the entire string: % matches zero or more characters; _ matches one PostgreSQL character, not one UTF-16 code unit. In the actual parameter use \%, \_ and \\ for literal percent, underscore and backslash. Java string literals require the usual additional escaping. A trailing unpaired escape is an invalid PostgreSQL LIKE pattern.

Searches are case-sensitive with deterministic PostgreSQL C collation and no Unicode normalization. Absent fields never match. An empty literal fragment matches every present string; an empty LIKE pattern matches only the empty string. Parameters are bound, never inserted as SQL text.

No schema migration is needed. Leading wildcard searches may scan many rows; there is no index or performance guarantee. Case-insensitive searches, regular expressions and configurable collations remain deferred.

Boolean expressions

Predicates can be composed with and, or, prefix not and parentheses. Precedence is not, then and, then or. Repeated not is allowed. All branches are type-checked; SQL evaluation order and short-circuiting are not guaranteed.

find Tours selected(TourStatus status, Title title)
    where (status = :status or title = :title) and not (reference is absent)
    order by title;

SQL three-valued logic applies: a comparison against an absent field is unknown, including under not. Only true matches are returned. To include absent references, write reference is absent or not (reference = :value). not (reference is absent) is equivalent to reference is present. Grouping changes neither result cardinality nor sorting rules. Values remain bound parameters. No schema migration is needed.