dtql

package
v0.87.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 23, 2026 License: MIT Imports: 10 Imported by: 0

README

DTQL — YAML serialization of dal.StructuredQuery

DTQL is a 1:1, lossless, human-readable YAML serialization of dalgo's dal.StructuredQuery for the core relational read-only subset. It is plain YAML over the existing dal query model — there is no bespoke grammar and no hand-written parser; the standard gopkg.in/yaml.v3 library produces and consumes it.

data, err := dtql.Serialize(q)   // dal.StructuredQuery -> DTQL-YAML
q2, err := dtql.Deserialize(data) // DTQL-YAML -> dal.StructuredQuery
dtql.Equal(q, q2)                 // structural equality over the covered surface

This package imports dal but adds no YAML dependency to dal.

Covered subset

DTQL represents a recursive From relation tree over root CollectionRef (Parent() == nil), Where, GroupBy, Having, OrderBy, Limit/Offset, and the final Columns projection. Expressions include fields, constants, arrays, parameters, arithmetic, aggregate functions and COUNT(*). Anything outside this subset — CollectionGroupRef / parented CollectionRef, a cursor (StartFrom), or an operator outside the in-scope set — is rejected by Serialize with a descriptive error rather than silently dropped.

Node → YAML mapping

dal node YAML representation
From over root CollectionRef from: { schema?: <string>, name: <string>, alias?: <string>, joins?: [<join>, ...] }
JoinedSource `{ type?: inner
Column a sequence item under columns:, an expression plus optional as: <alias>
wildcard exclusion projection { wildcard: { source?: <alias-or-name>, exclude: [<column>, ...] } } under columns:
Comparison { op: <operator>, left: <expr>, right: <expr> }
GroupCondition (And) { and: [ <condition>, ... ] }
GroupCondition (Or) { or: [ <condition>, ... ] }
OrderExpression a sequence item under orderBy:, an expression plus optional desc: true
GroupBy groupBy: [ <expression>, ... ]
money amount policy money: {minorUnitScale: 2, divisionScale: 4, rounding: halfEven}
Having having: <condition>

The optional money policy applies to federated streaming aggregates. Amounts are canonical decimal strings or whole integers; fractional binary floats and amounts with more digits than minorUnitScale are rejected. SUM accumulates integer minor units and returns decimal text. AVG and binary per-capita division return decimal text rounded half-even to divisionScale; division by zero errors. Existing queries keep their numeric behavior. The strongo/decimal cents type cannot serve as this accumulator because its int64 range excludes large sums and its current parser passes through binary floating point. | FieldRef | { field: <name>, source?: <alias> } | | Constant | { value: <scalar> } (inline string, bool, int or float) | | Array | { values: [ <scalar>, ... ] } (inline, for In membership) | | Param | { param: <name> } — a runtime parameter ($name in query text), substituted with a constant or array before execution; names are dotted identifiers such as currentUser or principal.roles | | aggregate | { aggregate: { function: count|sum|avg|min|max|first|last, distinct?: true, args: [<expr>] } } | | arithmetic | { binary: { op: +|-|*|/, left: <expr>, right: <expr> } } | | COUNT(*) argument | { star: true } | | Operator | the dal.Operator string itself: ==, In, >, >=, <, <= | | Limit / Offset | limit: <int> / offset: <int> (omitted when zero) |

An expression node sets exactly one discriminator. Aggregate arguments are expressions, so SUM(quantity * unit_price) needs no later AST redesign. Qualified fields keep the source structural, for example {field: country, source: c}; the source is never flattened into a dotted field name.

Recursive joins

Every relation node may contain ordered joins. A join's from is another relation node, on contains one or more == predicates, and every ON operand is a qualified field reference. inner is the default; left is the other supported type. The serializer emits alias and ==; deserialization also accepts as and eq and normalizes them on output.

from:
  name: Invoice
  alias: i
  joins:
    - from:
        name: Customer
        alias: c
        joins:
          - type: left
            from: {name: Employee, alias: e}
            on:
              - left: {field: SupportRepId, source: c}
                op: '=='
                right: {field: EmployeeId, source: e}
            hints:
              algorithms: [nestedLoop, hash]
      on:
        - left: {field: CustomerId, source: i}
          op: '=='
          right: {field: CustomerId, source: c}
      hints:
        algorithms: [merge, hash]

Each JOIN can independently request an ordered list of hash, merge, lookup, batchedLookup, and nestedLoop. Lists must be nonempty and contain distinct, case-sensitive names. The generic executor currently selects hash for a direct cross-side key and nestedLoop for bounded candidate evaluation. It skips unavailable algorithms and uses its ordinary strategy when no hint applies. A preferred nestedLoop bypasses an available hash index and can hit the candidate bound after up to O(N × M) comparisons for N left and M right rows. Hints never change successful query results or their order.

Validation reports stable JOIN category and zero-based path pairs, such as join_scope at from.joins[1].on[0]. It checks relation shape, inner/left, qualified equality predicates, duplicate aliases, forward references, lexical scope, and cyclic in-memory relation trees. Field existence and key runtime types require schema or execution data and are validated by later layers.

Aggregation

DTQL keeps projection at the end of the YAML pipeline:

from:
  name: orders
where:
  op: ==
  left: {field: status}
  right: {value: paid}
groupBy:
  - field: country
having:
  op: '>'
  left: {field: revenue}
  right: {value: 10000}
orderBy:
  - field: revenue
    desc: true
columns:
  - field: country
  - aggregate:
      function: count
      args: [{star: true}]
    as: orders
  - aggregate:
      function: count
      distinct: true
      args: [{field: customer_id}]
    as: customers
  - aggregate:
      function: sum
      args: [{field: total}]
    as: revenue

With groupBy and no columns, grouping expressions are selected implicitly. Without groupBy, aggregate columns operate on one implicit group. WHERE filters rows; HAVING filters groups; requested ordering, offset and limit apply to aggregate rows.

COUNT(*) counts rows. Other aggregates ignore nulls except FIRST/LAST, where null is a legitimate first/last value. Empty ungrouped input returns one row (COUNT(*) = 0, other aggregates null); empty explicit grouping returns no rows. SUM/AVG use finite float64 accumulation and COUNT returns int64. Arithmetic normalizes numeric operands to float64; non-numeric operands and division by zero evaluate to null. FIRST/LAST require a provider-declared stable input order; aggregate-local ordering is a future extension.

Negative projection

A wildcard column item may exclude one or more unqualified column names. The unqualified form selects every available column except the requested names:

from:
  name: customers
columns:
  - wildcard:
      exclude:
        - email
        - password_hash

When the source has an alias, source scopes the wildcard while the names in exclude remain unqualified:

from:
  name: customers
  alias: c
columns:
  - wildcard:
      source: c
      exclude:
        - email

An excluded name that is absent from the result schema is ignored. Duplicate names are harmless, and removing columns preserves the relative order of every remaining column. This makes defensive requests such as excluding password, password_hash, secret, and api_key useful without prior schema discovery. It is a projection convenience, not a security boundary; access control and sensitive-data policy remain separate concerns. The AST retains the requested exclusion list so future response metadata can distinguish matched and unmatched exclusions without changing query semantics.

Document shape

A DTQL-YAML document has this canonical key order (top-level keys are omitted when empty/zero, except from which is required):

from:
  name: users
where:
  and:
    - op: '>='
      left:
        field: age
      right:
        value: 18
    - or:
        - op: In
          left:
            field: status
          right:
            values:
              - active
              - pending
        - op: ==
          left:
            field: country
          right:
            value: US
orderBy:
  - field: name
  - field: age
    desc: true
limit: 10
offset: 20
columns:
  - field: name
  - field: age
    as: years

For a schema-qualified source, schema is a separate optional identifier:

from:
  schema: main
  name: Customer
limit: 50

The same shape represents SQL Server dbo.Customer by setting schema: dbo. Schema values are not special-cased. SQL adapters must quote the schema and relation as separate identifier segments, such as "main"."Customer" for SQLite or [dbo].[Customer] for SQL Server. Non-SQL adapters that cannot represent a schema-qualified source must reject it explicitly; they must not silently flatten the two segments into a dotted collection name.

DALgo access policies likewise do not collapse a qualified source onto the ordinary root collection path. Secured sessions classify it as an opaque query, so execution requires an explicit OpaqueQueryScope rule until schema-aware path resources are defined.

Round-trip guarantees

  • Structural — Deserialize(Serialize(q)) reconstructs a StructuredQuery structurally equal to q across From, Where, GroupBy, Having, OrderBy, Limit/Offset, and Columns (see dtql.Equal).
  • Canonical — Serialize emits a canonical document (stable key order and 2-space indentation). Serialize(Deserialize(d)) normalizes accepted input aliases (as to alias, eq to ==) and omitted INNER types, then remains byte-identical on later round trips.

Errors

Deserialize returns a descriptive error and no partially-populated query on malformed or schema-invalid input: unknown keys, wrong value types, a missing required from.name, an unknown operator, a comparison missing left/right, an expression that does not select exactly one expression form, or a condition that mixes the comparison and group forms.

Published artifacts

The package also publishes DTQL as a tool-validatable artifact: a JSON Schema (draft 2020-12) generated from the Go types — schema/schema.json (canonical $id https://dal-go.github.io/dtql/schema.json) and schema/schema.yaml — a set of example documents under examples/, and a styled index page, served at https://dal-go.github.io/dtql/.

Documentation

Overview

Package dtql provides a 1:1, lossless, human-readable YAML serialization of dalgo's dal.StructuredQuery for the core relational read-only subset.

DTQL ("DataTug Query Language") is plain YAML over the existing dal query model — there is no bespoke grammar and no hand-written parser; a standard YAML library produces and consumes it. The package serializes an in-scope dal.StructuredQuery to a DTQL-YAML document and deserializes it back, proving a lossless round-trip in both directions.

The covered subset and its node→YAML mapping are documented in dtql/README.md.

This package imports dal but adds no YAML dependency to dal.

Index

Constants

View Source
const SchemaID = "https://dal-go.github.io/dtql/schema.json"

SchemaID is the canonical $id of the published DTQL JSON Schema.

Variables

This section is empty.

Functions

func Deserialize

func Deserialize(data []byte) (dal.StructuredQuery, error)

Deserialize reconstructs a dal.StructuredQuery from a DTQL-YAML document. Malformed or schema-invalid input — unknown keys, wrong value types, missing required fields, or an unknown operator — yields a descriptive error and no (partially-populated) query.

func Equal

func Equal(a, b dal.StructuredQuery) bool

Equal reports whether two in-scope dal.StructuredQuery values are structurally equal across the DTQL-covered surface: From, Columns, the Where condition tree (including inline Constant/Array values), OrderBy, Limit and Offset. It compares via the dal accessor interface, so it is agnostic to the concrete query type (e.g. a builder query vs. a deserialized one). It is the equality mechanism behind the structural round-trip guarantee. The in-scope dal nodes are value types (Comparison, GroupCondition, FieldRef, Constant, Array), so the type switches handle the value forms.

func SchemaJSON

func SchemaJSON() []byte

SchemaJSON renders the DTQL JSON Schema as canonical, indented JSON. The schema document holds only strings, bools, ints, slices and maps, so marshaling cannot fail.

func SchemaYAML

func SchemaYAML() []byte

SchemaYAML renders the DTQL JSON Schema as canonical YAML (identical content).

func Serialize

func Serialize(q dal.StructuredQuery) ([]byte, error)

Serialize converts an in-scope dal.StructuredQuery into a canonical DTQL-YAML document. queryToDocument is the single validating pass: an out-of-scope query yields a descriptive error and no document.

Types

This section is empty.

Directories

Path Synopsis
Package authorization defines the portable DTQL authorization result wire contract.
Package authorization defines the portable DTQL authorization result wire contract.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL