tugql

package
v0.102.4 Latest Latest
Warning

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

Go to latest
Published: Nov 12, 2026 License: MIT Imports: 18 Imported by: 12

README

TugQL YAML and TugQL text authoring

The existing TugQL Serialize and Deserialize APIs provide a 1:1, human-readable YAML serialization of dalgo's dal.StructuredQuery for the core relational read-only subset. TugQL YAML remains plain YAML over the existing dal query model and uses the standard gopkg.in/yaml.v3 library.

The TugQL text form is an additive text-authoring adapter with a versioned TugQLDocument/TugQLTree model. ParseTugQL parses source, ResolveTugQL validates it against caller-authorized schemas and lowers supported constructs into the same dal.StructuredQuery, and FormatTugQL formats the source. It does not replace or change the TugQL YAML Serialize/Deserialize APIs. The TugQL grammar, supported resolution profile, and approval record are in spec/features/tugql/README.md.

package main

import (
	"fmt"

	"github.com/dal-go/dalgo/tugql"
)

func main() {
	doc, diagnostics := tugql.ParseTugQL("from Invoice as i\nselect i.Id\n")
	if len(diagnostics) != 0 {
		fmt.Println(diagnostics)
		return
	}
	context := tugql.TugQLResolveContext{
		AuthorizedSchemas: []tugql.TugQLAuthorizedSchema{{
			Version: "schema-r1",
			Tables: []tugql.TugQLTable{{
				Name: "Invoice",
				Fields: []tugql.TugQLField{{Name: "Id", Type: "integer", Authorized: true}},
			}},
		}},
	}
	resolved, diagnostics := tugql.ResolveTugQL(doc, context)
	if len(diagnostics) != 0 {
		fmt.Println(diagnostics)
		return
	}
	fmt.Printf("resolved %d output column(s)\n", len(resolved.Columns))
}
TugQL syntax

TugQL puts parameters first and the optional projection last. This example combines a named CTE with a correlated scalar subquery:

parameters (
  @CustomerId integer required
)
with Invoices as (
  from Invoice as i -- billing records
  where i.CustomerId = @CustomerId
  select (
    i.InvoiceId
    i.CustomerId
  )
)
from Invoices as i
select (
  i.InvoiceId as ID
  CustomerName as (
    from Customer as c
    where c.CustomerId = i.CustomerId
    select c.LastName
  )
)

Use one parameter or multiline projection item per line, without commas. Compact projections remain available: select i.InvoiceId, i.CustomerId. Block openers stay on their header line and closers have their own aligned line. Indent each level with two spaces or one tab, consistently within the query. Reserved words must be consistently lowercase or uppercase; identifiers retain their spelling. as is mandatory for aliases and CTEs. Ordinary fields use expression as Alias; scalar subqueries put the result name before as (.

Saved queries use with Saved from './saved.tql', with an indented using ( block for explicit @Parameter = @CallerParameter or scalar literal mappings. The caller supplies project-relative contents pinned to its project revision. An omitted JOIN on, or on CustomerId, resolves only when caller-authorized relationship metadata identifies exactly one complete matching relationship. An omitted select expands authorized fields in order and merges guaranteed equal INNER-join keys while preserving both fields' lineage.

A from or join source is a dotted path (sales.Invoice) or a slash path (spaces/{SpaceID}/ext/datatug/projects); one path may not mix . and /. The last segment is the collection name and the earlier ones are its schema (spaces/{SpaceID}/ext/datatug). A segment is a word, a non-empty quoted identifier without {, } or /, or (in a slash path only) a whole {name} substitution kept literally in the tree. A clause keyword used as a segment must be quoted (spaces/"from"/x). / must touch both neighbouring segments, and a // touching a path segment is an empty_path_segment error rather than a comment (from users // x is still a comment). A schema containing / prints back in slash form. The shared parser vectors are in testdata/slash-paths/cases.json, listed in testdata/slash-paths/manifest.sha256. A tree segment the text form cannot spell fails PrintTugQL with unsupported_print.

WHERE filters source rows before grouping; HAVING filters completed groups after GROUP BY. The clause order is FROM, optional WHERE, optional GROUP BY, optional HAVING, optional ORDER BY and LIMIT, then optional final SELECT:

parameters (
  @MinInvoices integer default 2
)
from Invoice as i
where i.InvoiceId > 1
group by i.CustomerId
having COUNT(*) >= @MinInvoices
order by i.CustomerId asc
select i.CustomerId, COUNT(*) as InvoiceCount

ResolveTugQL binds the parameter default, validates the grouping, and lowers the predicate into the existing dal.StructuredQuery. The generic DALgo executor applies WHERE to invoice rows, computes each customer's COUNT(*), keeps groups whose count is at least the threshold, then orders and projects the result. This count example avoids assumptions about exact money arithmetic.

Cross-database sources

One query may read several databases. An optional root databases ( block, before parameters, declares one alias per line as alias or alias = '<target>'; a from or join source at any level (main query, CTE, scalar subquery) is qualified as alias::path:

databases (
  sales = 'datatug://chinook/sales?env=prod'
  geo
)
parameters (
  @MinSales integer default 1000
)
with BigInvoices as (
  from sales::Invoice as i
  where i.Total > 10
  select i.InvoiceId, i.BillingCountry, i.Total
)
from BigInvoices as b
join geo::Country as c
  on b.BillingCountry = c.Name
group by c.Name
having SUM(b.Total) >= @MinSales
select (
  c.Name as Country
  SUM(b.Total) as TotalSales
)

An alias is an unquoted word that is not a reserved keyword, unique case-insensitively; a qualifier names it exactly, with :: touching both sides. A target is a non-empty single-quoted string, opaque to DALgo (it must be quoted because // starts a comment); an alias without a target is bound by the caller. A target carrying a password fails with database_credentials (the message names only the alias). The check is textual, not URL parsing:

  1. at the start of the target and right after every ://, take the text that follows; when it contains @ and the part before its first @ has no /, that part is user information and must not contain : (postgres://u:secret@host/db, u:secret@tcp(host:3306)/db, postgres://u:secret@host/db?redirect=http://x);
  2. password, pwd or passwd, in any case, then optional spaces or tabs and =, must not appear at the start of the target or right after ;, &, ?, ,, a space or a tab (Server=h;Password=x, Server=h,Password=x, postgres://h/db?password=x).

postgres://reader@host/db and sqlite:/data/a@b.db are accepted.

An unqualified source reads the caller's default database. When the document declares databases, it matches only authorized schemas whose database is empty; without declarations it matches any database, as before. dal.ExecuteFederatedQuery requires every source to be qualified (an unqualified one fails closed with "has no database"), so a caller that wants a default database in a federated query binds it as an alias. The tree carries databases: [{name, target?}] (TugQLTree.Databases) and each qualified source's database, which resolution matches only against authorized schemas of that database and lowers to dal.NewDatabaseCollectionRef; dal.ExecuteFederatedQuery then routes each source by alias through the caller's DatabaseResolver. A join between sources of different databases must state its on: relationship metadata names no database, so an omitted on there is cross_database_relationship.

Other diagnostics: invalid_databases and unclosed_databases (block shape), invalid_database (declaration), invalid_database_qualifier (spaces around :: or a quoted alias), duplicate_database, undeclared_database (on the qualifier), unused_database (on the declaration), and nested_databases for a block anywhere but the start of the root document, including an imported saved query. A caller-built tree with no declarations keeps its meaning for ResolveTugQL, but PrintTugQL requires every qualified source to be declared. The shared Go/TypeScript vectors are testdata/tugql/v1/cross-database.json.

Comments

In the text form -- and // each start a comment that runs to the end of the line. They are recognised only outside string literals and quoted identifiers, so 'https://example.com/x' and "a//b" contain no comment. A // touching a from/join path segment is an error, not a comment (see above). # is not a comment in the text form. FormatTugQL keeps every comment and the author's marker.

Comments are carried on the parsed tree (TugQLTree.Comments), not on dal.StructuredQuery, and never influence resolution or generated SQL. SQL emitters must not write //; a comment an emitter carries is written as --.

The structured YAML form uses YAML # comments:

Text TugQL YAML
comment lines above a clause head comment above the clause key
comment after code on a clause line same-line comment of that clause
comment after the last clause document-end comment
data, diagnostics := tugql.TugQLToYAML("-- invoices\nfrom Invoice // main table\nselect Id")
// query:
//     # invoices
//     from: # main table
//         name: Invoice
//     columns:
//         - field: Id
text, diagnostics := tugql.YAMLToTugQL(data)
// -- invoices
// from Invoice -- main table
// select Id

Both markers become #; # becomes --. A // comment therefore comes back as -- after a text, YAML, text round trip. This is the one documented and accepted loss; the comment text and its placement are kept. A comment with no clean anchor (inside a multi-line PARAMETERS, WITH or SELECT block, or on a YAML node that is not a clause) becomes a leading comment of the nearest following clause, and a comment after the last clause becomes a document-end comment. A comment is never dropped. Blank lines between comments of one group are kept; a blank line before the first comment or between the last comment and its clause is not. The text after the marker loses one leading space and trailing whitespace; --x and -- x carry the same text.

The anchors are databases, parameters, definitions[i], query.from, query.from.joins[k], query.from.joins[k].on[j], query.where, query.groupBy, query.having, query.orderBy, query.limit and query.columns.

YAMLToTugQL and PrintTugQL print the canonical layout (lowercase keywords, two-space indentation, one-line SELECT list) and fail closed with a diagnostic for tree features that have no text spelling (for example offset, money, DISTINCT aggregates, IN/EXISTS conditions, negative literals).

The shared Go/TypeScript conformance vectors are testdata/tugql/v1/comments.json, listed in testdata/tugql/v1/manifest.sha256.

Resolution limits

TugQL resolution uses a 128-level structural-depth limit and a 5,000-node semantic budget. The aggregate budget counts the authored root document, each parsed pinned import occurrence once (repeated imports count once per occurrence), and any positive node-count growth from the authored root query to the final expanded root query. Each expanded CTE, import, and scalar-query body is also checked independently against 5,000 canonical nodes and 128 levels before another recursive walk or copy. Version 1 validates all declared definitions eagerly, including unused ones, and caps import expansion at 128 documents.

The sections below document the TugQL YAML mapping and behavior.

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

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

Covered subset

TugQL 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>, ... ] }
IsNullCondition { isNull: <expression> } / { isNotNull: <expression> }
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.

Null tests

isNull and isNotNull take one expression (usually a field) and are true when it is, respectively is not, null. They are valid wherever a condition is: in where, in having, inside and / or groups, and in a nested query's conditions; they are not valid in a join's on list, which stays equality only.

from: {name: Chat, alias: c, joins: [{type: left, from: {name: Invoice, alias: i}, on: [{left: {field: id, source: c}, op: '==', right: {field: chat, source: i}}]}]}
where:
  and:
    - isNull: {field: Company, source: c}      # no company, or the column is absent
    - isNotNull: {field: chat, source: i}       # ...and the chat has an invoice
columns: [{field: id, source: c}]
from: {name: sales}
groupBy: [{field: category}]
having:
  isNotNull: {aggregate: {function: max, args: [{field: amount}]}}
columns: [{field: category}]

A field missing from a document counts as null, like an explicit null or a nil pointer. A null test is never unknown, so or over it behaves as two-valued logic. The operand is a field, a literal, arithmetic over those, or a scalar subquery; an aggregate is allowed only in having, and a values list, star and param are rejected at parse (query_shape at where.isNull: ...). A document that sets two condition forms (isNull with isNotNull, and, op, ...) is rejected as mixing forms.

What op: == with {value: null} means, and why it is not enough. It does not mean one thing; it depends on where the condition is evaluated:

where the condition runs x == null x < 1, In, ... against null
where (and join on) of a query the generic executor runs: any join, nested query, or federated aggregation UNKNOWN: matches no row UNKNOWN
having (local aggregation) true when x is null, so having: g == null returns the null group < <= > >= false; In/NotIn unsupported
where of a single-source query handed whole to an adapter the adapter's meaning (IS NULL in the SQLite emitter; the in-memory adapter also matches an absent field) the adapter's meaning

So where: Company == null returns the null rows from one table and none once the query is joined, while having treats null as a value. That where versus having inconsistency predates null tests and this change leaves it alone; isNull / isNotNull are the one spelling whose meaning is the same in every row of that table. The serializer never rewrites an existing == null comparison, and != does not exist in TugQL, so isNotNull is the only way to ask for non-null values. Rendered as SQL a null test is x IS NULL / x IS NOT NULL.

Which engines evaluate it. The generic executor (joins, nested queries, aggregation, HAVING) evaluates null tests whatever adapter supplies the rows. An adapter that runs a query itself, a single-source query or one it claims natively (a native join or aggregation), must evaluate or translate them: the in-memory adapter (dalgo2memory) does, and every other adapter currently rejects the query with an "unsupported condition" error until it implements them (an adapter must fail rather than ignore a condition it cannot translate). The TypeScript engine parses a bare single-source query with a null test into its relation model so the same document runs there; see dalgo-js's README.

A misspelt field is not an error in Go unless the executor knows the schema. A record that lacks a field and a field that does not exist look the same, so isNull: {field: compnay} matches every row, and isNotNull none. When the backend reports its fields (dal.JoinFieldsProvider) the executor rejects an unknown field before reading a row (join_field), the way it does for a comparison; the TypeScript parser always has a schema and rejects it at parse. Without field metadata, check field names before running a null test.

A null list element is dropped, not rejected. and: [{isNull: ...}, null] reads as and: [{isNull: ...}], because the YAML decoder skips a null element of a list of mappings (this predates null tests and applies to every condition). The TypeScript parser rejects it.

Not in policies. Access policy predicates (access.DocumentCondition, condeval) keep the comparison and group subset and reject a null test: there a missing field never matches (layered-ACL acceptance V02), and isNull would make it match. The query engine reads a missing field as null, as it already does for every other operator.

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

TugQL 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 TugQL 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. A policy names such a source with a table rule (access.TableScope); a policy with no table rule needs an explicit OpaqueQueryScope rule for it.

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 tugql.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 TugQL 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/tugql/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/tugql/.

Documentation

Overview

Package tugql provides TugQL query authoring, validation, resolution, formatting, and serialization for dal.StructuredQuery.

Decision: TugQL has been renamed to TugQL. TugQL is the canonical name of the SQL-like query language and TugQL.org is its canonical public home. TugQL.org is retained as a permanent redirect for backwards compatibility and discovery. TugQT (Tug Query Tree) is the intended name for the structured query-tree concept.

TugQL provides a human-friendly SQL-like authoring syntax with a versioned TugQLDocument/TugQLTree model: ParseTugQL creates a TugQLDocument and TugQLTree, ResolveTugQL validates and lowers supported constructs through the underlying query model, and FormatTugQL formats source text. For example:

doc, diagnostics := tugql.ParseTugQL("from Invoice as i\nselect i.Id\n")
if len(diagnostics) != 0 {
	// show diagnostics to the author
}
resolved, diagnostics := tugql.ResolveTugQL(doc, context)

The existing Serialize and Deserialize APIs provide 1:1, lossless YAML serialization over dal.StructuredQuery (corresponding to the TugQT concept).

TugQL syntax, resolution, and formatting are described in spec/features/tugql/README.md. The YAML subset and node mapping are documented in tugql/README.md.

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

Index

Constants

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

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

Variables

This section is empty.

Functions

func Deserialize

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

Deserialize reconstructs a dal.StructuredQuery from a TugQL 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 DeserializeTugQL

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

DeserializeTugQL reconstructs a dal.StructuredQuery from a TugQL/TugQT-YAML document.

func Equal

func Equal(a, b dal.StructuredQuery) bool

Equal reports whether two in-scope dal.StructuredQuery values are structurally equal across the TugQL-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 ParseTugQL

func ParseTugQL(source string) (TugQLDocument, []TugQLDiagnostic)

ParseTugQL parses the complete version 1 source profile. On any diagnostic it returns the exact source and metadata without a semantic tree, preventing a partial parse from becoming executable.

func ResolveTugQL

func ResolveTugQL(doc TugQLDocument, context TugQLResolveContext) (TugQLResolved, []TugQLDiagnostic)

ResolveTugQL validates caller-provided semantic trees against authorized schema and bindings, lowers supported authoring constructs, then delegates executable query validation to the existing TugQL deserializer.

func SchemaJSON

func SchemaJSON() []byte

SchemaJSON renders the TugQL 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 TugQL 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 TugQL YAML document. queryToDocument is the single validating pass: an out-of-scope query yields a descriptive error and no document.

func SerializeTugQL

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

SerializeTugQL converts an in-scope dal.StructuredQuery into a canonical TugQL/TugQT-YAML document.

Types

type TugQLAuthorizedSchema

type TugQLAuthorizedSchema struct {
	Database string       `json:"database,omitempty"`
	Schema   string       `json:"schema,omitempty"`
	Version  string       `json:"version"`
	Tables   []TugQLTable `json:"tables"`
}

TugQLAuthorizedSchema is a schema version ResolveTugQL may read. When the query source names a database or schema, the authorized schema must carry the same value, and Version is joined into the resolved schema version.

type TugQLBinding

type TugQLBinding struct {
	Name  string `json:"name"`
	Set   bool   `json:"set"`
	Value any    `json:"value,omitempty"`
}

TugQLBinding is a caller-supplied value for a declared parameter. When Set is false the binding is ignored and does not satisfy a required parameter.

type TugQLBody

type TugQLBody struct {
	Definitions []TugQLDefinition `json:"definitions,omitempty" yaml:"definitions,omitempty"`
	Query       map[string]any    `json:"query" yaml:"-"`
}

TugQLBody is the body of a CTE or a scalar query. Definitions are optional and Query is required.

func (TugQLBody) MarshalYAML

func (body TugQLBody) MarshalYAML() (any, error)

MarshalYAML requires Query, validates the body and its definitions, and returns the definitions and query.

func (*TugQLBody) UnmarshalYAML

func (body *TugQLBody) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML checks the YAML node budget and body shape, then stores Definitions and Query.

type TugQLComment

type TugQLComment struct {
	Text        string `json:"text"`
	BlankBefore bool   `json:"blankBefore,omitempty"`
}

TugQLComment is one source comment with its marker removed. In the TugQL text form a comment starts with `--` or `//` and runs to the end of the line; in the structured YAML form it is a `#` comment. Text drops the marker and one optional following space, so `-- note`, `--note` and `// note` all carry the text "note". BlankBefore records that at least one blank line separated this comment from the previous comment of the same group; it is never set on the first comment of a group.

type TugQLCommentGroup

type TugQLCommentGroup struct {
	Leading  []TugQLComment `json:"leading,omitempty"`
	Trailing *TugQLComment  `json:"trailing,omitempty"`
}

TugQLCommentGroup holds the comments attached to one anchor: the comment lines above the anchor (Leading) and the comment on the anchor's own line (Trailing).

type TugQLComments

type TugQLComments struct {
	Anchors map[string]TugQLCommentGroup `json:"anchors,omitempty"`
	End     []TugQLComment               `json:"end,omitempty"`
}

TugQLComments carries every comment of a TugQTree. Comments are not part of the query semantics: they live beside the tree, are ignored by resolution, and never reach `dal.StructuredQuery` or generated SQL.

Anchors maps a TugQTree path to the comments attached to the element at that path. The anchorable elements are the clause-level elements a person writes on one line of text:

databases                        the DATABASES block
parameters                       the PARAMETERS block
definitions[i]                   the i-th WITH definition
query.from                       the FROM line
query.from.joins[k]              the k-th JOIN line
query.from.joins[k].on[j]        the j-th ON line of join k
query.where  query.groupBy  query.having  query.orderBy  query.limit
query.columns                    the SELECT clause

A comment with no clean anchor (inside a multi-line block, or in a position of the YAML form that is not an anchor) becomes a leading comment of the nearest following anchor. A comment after the last anchor is kept in End. A comment is never dropped.

type TugQLDatabase added in v0.102.0

type TugQLDatabase struct {
	Name   string `json:"name" yaml:"name"`
	Target string `json:"target,omitempty" yaml:"target,omitempty"`
}

TugQLDatabase declares a database alias of a TugQTree, written in the text form as one line of the root DATABASES block. Name is an unquoted word that is not a reserved keyword; names are unique case-insensitively. Target is an optional opaque reference (for example `datatug://chinook/sales`) that must not carry a password; when it is empty the caller binds the alias. A source reads a declared database by carrying its Name as `database`.

type TugQLDefinition

type TugQLDefinition struct {
	Kind  string         `json:"kind" yaml:"kind"`
	Name  string         `json:"name" yaml:"name"`
	Query *TugQLBody     `json:"query,omitempty" yaml:"query,omitempty"`
	Path  string         `json:"path,omitempty" yaml:"path,omitempty"`
	Using []TugQLMapping `json:"using,omitempty" yaml:"using,omitempty"`
}

TugQLDefinition is a named CTE or import. Kind is cte or import. A CTE has Query and no Path or Using, and an import has Path and no Query.

type TugQLDependencyReceipt

type TugQLDependencyReceipt struct {
	Path     string `json:"path"`
	Revision string `json:"revision"`
}

TugQLDependencyReceipt records the project path and revision of a saved-query import ResolveTugQL expanded.

type TugQLDiagnostic

type TugQLDiagnostic struct {
	Code    string    `json:"code"`
	Message string    `json:"message"`
	Span    TugQLSpan `json:"span"`
}

TugQLDiagnostic is one parse, format, or resolve finding. Code and Message describe it, and Span locates it in the source.

func FormatTugQL

func FormatTugQL(doc TugQLDocument, options TugQLFormatOptions) (string, []TugQLDiagnostic)

FormatTugQL formats version 1 source while preserving comments and expressions. Lexically invalid source is returned unchanged with diagnostics so an editor can keep a repairable buffer.

func PrintTugQL

func PrintTugQL(tree TugQLTree) (string, []TugQLDiagnostic)

PrintTugQL renders a TugQTree, including its comments, as TugQL source text.

func TugQLToYAML

func TugQLToYAML(source string) ([]byte, []TugQLDiagnostic)

TugQLToYAML converts TugQL source text into the structured TugQL YAML form. Every `--` and `//` comment becomes a `#` comment: a comment on its own lines is the head comment above the clause it precedes, and a comment after code is the same-line comment of that clause. On any diagnostic no YAML is returned.

func YAMLToTugQL

func YAMLToTugQL(data []byte) (string, []TugQLDiagnostic)

YAMLToTugQL converts the structured TugQL YAML form into TugQL source text. Every `#` comment becomes a `--` comment, so a `//` comment written in text comes back as `--` after a text, YAML, text round trip. The output uses the canonical lowercase, two-space layout. Constructs that have no text spelling fail closed with a diagnostic.

type TugQLDocument

type TugQLDocument struct {
	Source         string              `json:"source,omitempty"`
	Tree           *TugQLTree          `json:"tree,omitempty"`
	SourceMetadata TugQLSourceMetadata `json:"sourceMetadata"`
}

TugQLDocument keeps the original authoring source separate from its versioned semantic TugQTree. Source may be empty when the caller starts from TugQTree YAML alone.

func (*TugQLDocument) UnmarshalJSON

func (doc *TugQLDocument) UnmarshalJSON(data []byte) error

UnmarshalJSON rejects unknown public document fields so JSON input follows the same closed-shape boundary as TugQTree YAML.

type TugQLField

type TugQLField struct {
	Name       string `json:"name"`
	Type       string `json:"type"`
	Authorized bool   `json:"authorized"`
}

TugQLField is one field of an authorized table. Authorized reports whether resolution may use the field.

type TugQLFormatOptions

type TugQLFormatOptions struct {
	KeywordCase            string `json:"keywordCase,omitempty"` // explicit lowercase, uppercase, or preserve-existing
	Indentation            string `json:"indentation,omitempty"` // explicit two-spaces, tab, or preserve-existing
	ProjectTeamKeywordCase string `json:"projectTeamKeywordCase,omitempty"`
	UserKeywordCase        string `json:"userKeywordCase,omitempty"`
	DefaultKeywordCase     string `json:"defaultKeywordCase,omitempty"`
	ProjectTeamIndentation string `json:"projectTeamIndentation,omitempty"`
	UserIndentation        string `json:"userIndentation,omitempty"`
	DefaultIndentation     string `json:"defaultIndentation,omitempty"`
}

TugQLFormatOptions carries keyword-case and indentation preferences for FormatTugQL. An explicit value and the matching project-team value must be equal when both are set, and preserve-existing keeps the style already in the source. Keyword case is lowercase, uppercase, or preserve-existing, and indentation is two-spaces, tab, or preserve-existing.

type TugQLMapping

type TugQLMapping struct {
	Name       string         `json:"name" yaml:"name"`
	Expression map[string]any `json:"expression" yaml:"expression"`
}

TugQLMapping pairs a parameter Name with an Expression in an import Using list.

type TugQLOutputColumn

type TugQLOutputColumn struct {
	Name    string               `json:"name"`
	Type    string               `json:"type"`
	Lineage []TugQLOutputLineage `json:"lineage"`
}

TugQLOutputColumn is one column reported by ResolveTugQL. Name is the output name, Type is the inferred type, and Lineage lists the source fields behind the column.

type TugQLOutputLineage

type TugQLOutputLineage struct {
	Source string `json:"source"`
	Field  string `json:"field"`
}

TugQLOutputLineage names a source field that an output column comes from.

type TugQLParameter

type TugQLParameter struct {
	Name     string `json:"name" yaml:"name"`
	Type     string `json:"type" yaml:"type"`
	Required bool   `json:"required,omitempty" yaml:"required,omitempty"`
	Default  *any   `json:"default,omitempty" yaml:"default,omitempty"`
}

TugQLParameter is a named parameter on a TugQTree. Name and Type are required, names are unique, and Type is integer, decimal, string, boolean, date, datetime, or timestamp. A required parameter cannot also have a default, and a default cannot be null.

type TugQLPinnedImport

type TugQLPinnedImport struct {
	Path     string `json:"path"`
	Revision string `json:"revision"`
	Source   string `json:"source"`
}

TugQLPinnedImport is the source text of a saved query at a project path and revision. ResolveTugQL parses Source when Path matches the import and Revision equals ProjectRevision.

type TugQLPosition

type TugQLPosition struct {
	Line   int `json:"line"`
	Column int `json:"column"`
}

TugQLPosition is a line and column in TugQL source. Both numbers start at 1.

type TugQLRelationEndpoint

type TugQLRelationEndpoint struct {
	Source string `json:"source,omitempty"`
	Table  string `json:"table"`
}

TugQLRelationEndpoint is one side of a relationship. Table matches a query source, and Source matches that source's alias when it is set.

type TugQLRelationship

type TugQLRelationship struct {
	ID                 string                  `json:"id"`
	Version            string                  `json:"version"`
	From               TugQLRelationEndpoint   `json:"from"`
	To                 TugQLRelationEndpoint   `json:"to"`
	Pairs              []TugQLRelationshipPair `json:"pairs"`
	ExactTypedEquality bool                    `json:"exactTypedEquality"`
}

TugQLRelationship is a join relationship supplied to ResolveTugQL. Resolution uses it only when ExactTypedEquality is set and each pair has the same authorized type on both ends.

type TugQLRelationshipExpansion

type TugQLRelationshipExpansion struct {
	ID         string                  `json:"id"`
	Version    string                  `json:"version"`
	FromSource string                  `json:"fromSource"`
	ToSource   string                  `json:"toSource"`
	JoinType   string                  `json:"joinType"`
	Pairs      []TugQLRelationshipPair `json:"pairs"`
}

TugQLRelationshipExpansion records a relationship ResolveTugQL applied to a join. It keeps the relationship id and version, the source aliases, the join type, and the field pairs.

type TugQLRelationshipPair

type TugQLRelationshipPair struct {
	FromField string `json:"fromField"`
	ToField   string `json:"toField"`
}

TugQLRelationshipPair names the FromField and ToField compared by a relationship.

type TugQLResolveContext

type TugQLResolveContext struct {
	AuthorizedSchemas []TugQLAuthorizedSchema `json:"authorizedSchemas,omitempty"`
	Relationships     []TugQLRelationship     `json:"relationships,omitempty"`
	ProjectRoot       string                  `json:"projectRoot,omitempty"`
	ImportingPath     string                  `json:"importingPath,omitempty"`
	ProjectRevision   string                  `json:"projectRevision,omitempty"`
	PinnedImports     []TugQLPinnedImport     `json:"pinnedImports,omitempty"`
	Bindings          []TugQLBinding          `json:"bindings,omitempty"`
	// contains filtered or unexported fields
}

TugQLResolveContext supplies the schemas, relationships, pinned imports, and bindings ResolveTugQL uses. Imports also require ProjectRoot, ImportingPath, and ProjectRevision.

type TugQLResolved

type TugQLResolved struct {
	Query         dal.StructuredQuery          `json:"query"`
	Columns       []TugQLOutputColumn          `json:"columns"`
	SchemaVersion string                       `json:"schemaVersion"`
	Dependencies  []TugQLDependencyReceipt     `json:"dependencies,omitempty"`
	Relationships []TugQLRelationshipExpansion `json:"relationships,omitempty"`
}

TugQLResolved is the value returned by ResolveTugQL. Query is the lowered dal.StructuredQuery and Columns describe its projection. SchemaVersion joins the matched schema versions with commas, and Dependencies and Relationships list the imports and joins that were applied.

type TugQLSourceMetadata

type TugQLSourceMetadata struct {
	Format  string `json:"format"`
	Version int    `json:"version"`
}

TugQLSourceMetadata records the authoring format and version of a TugQLDocument. ResolveTugQL requires format "tugql" and version 1.

type TugQLSpan

type TugQLSpan struct {
	Start TugQLPosition `json:"start"`
	End   TugQLPosition `json:"end"`
}

TugQLSpan is the start and end of a range in TugQL source.

type TugQLTable

type TugQLTable struct {
	Name   string       `json:"name"`
	Fields []TugQLField `json:"fields"`
}

TugQLTable is a named table and its fields in an authorized schema.

type TugQLTree

type TugQLTree struct {
	Format      string            `json:"format" yaml:"format"`
	Version     int               `json:"version" yaml:"version"`
	Databases   []TugQLDatabase   `json:"databases,omitempty" yaml:"databases,omitempty"`
	Parameters  []TugQLParameter  `json:"parameters,omitempty" yaml:"parameters,omitempty"`
	Definitions []TugQLDefinition `json:"definitions,omitempty" yaml:"definitions,omitempty"`
	Query       map[string]any    `json:"query" yaml:"-"`
	// Comments holds the author's comments. They are not semantics: resolution,
	// authorization and SQL generation never read them.
	Comments *TugQLComments `json:"comments,omitempty" yaml:"-"`
}

TugQLTree is a versioned semantic wrapper around the existing TugQL query document. Query maps use the same field names and discriminators as TugQL YAML.

func (TugQLTree) MarshalYAML

func (tree TugQLTree) MarshalYAML() (any, error)

MarshalYAML allows standard yaml.Marshal(tree) to emit the canonical versioned TugQTree form while retaining the legacy TugQL query field order.

func (*TugQLTree) UnmarshalYAML

func (tree *TugQLTree) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML validates the envelope shape but leaves query validation to ResolveTugQL, which also checks a caller-mutated public tree. It also reads the YAML comments into Comments. yaml.Unmarshal passes only the root mapping, so comments attached to the YAML document node (above a blank line at the top, or the document-end comments) are visible only through YAMLToTugQL.

Directories

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

Jump to

Keyboard shortcuts

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