Skip to content

DSL v3 — regular syntax

This page defines the public query syntax. Data structures, ownership, and state transitions are defined in the common interface.

RuleDefinition
Explicit names<col>(value) means equality. Other operators are part of the method name: <col>NotEq, <col>Gt, <col>In, and <col>IsNull. relation, relations, join, and leftJoin identify different operations.
SQL termsUse select, and, or, join, leftJoin, on, orderBy, limit, and groupBy names.
Shared tokensPHP, Go, Rust, and TypeScript use the same logical token names. Language casing follows local syntax.
Groupingor() changes the connection for the next item. and(fn) and or(fn) create a nested group.
IDE checksGenerated builders expose only columns and operators allowed by the schema. Where callbacks receive the generated entity Where type.
ExecutionA root query receives its executor with using. Terminal methods receive values only.

1. Structure

A query is built first, bound to an executor, and terminated with a value operation. The executor is inherited by joins, relation stages, and returned rows. Executing without an executor or after its transaction ends returns CONFIG.

php
$battle = Battle::query()->using($db);
$count = $battle->getCountByServiceSeq(7);
go
battle := gen.Battle().Using(ctx, db)
count, err := battle.GetCountByServiceSeq(7)
rust
let battle = battle::query().using(&db);
let count = battle.get_count_by_service_seq(7).await?;
ts
const battle = Battle().using(database);
const count = await battle.getCountByServiceSeq(7);

The call order is:

text
Query() → using(executor) → select/join/where/relation → order/limit → get/gets/getCount

Rows are separate generated types. A row provides getters, setters, update, updateOptimistic, delete, and deleteCascade according to the schema.

Keyset pagination

getsAfter(cursor, per) and getsBefore(cursor, per) return KeysetPage<Row>. The first call uses an empty cursor. The query order must contain table columns only; the generator appends missing primary-key columns in ascending order. The cursor stores the normalized order and typed cursor values. per must be positive and offset pagination cannot be combined with a keyset call.

php
$first = Battle::query()->orderBySeqAsc()->using($db)->getsAfter('', 20);
$next = Battle::query()->orderBySeqAsc()->using($db)->getsAfter($first->nextCursor, 20);
$previous = Battle::query()->orderBySeqAsc()->using($db)->getsBefore($next->previousCursor, 20);
go
first, err := gen.Battle().OrderBySeqAsc().Using(ctx, db).GetsAfter("", 20)
next, err := gen.Battle().OrderBySeqAsc().Using(ctx, db).GetsAfter(first.NextCursor, 20)
previous, err := gen.Battle().OrderBySeqAsc().Using(ctx, db).GetsBefore(next.PreviousCursor, 20)
rust
let first = battle::query().order_by_seq_asc().using(&db).gets_after("", 20).await?;
let next = battle::query().order_by_seq_asc().using(&db).gets_after(&first.next_cursor, 20).await?;
let previous = battle::query().order_by_seq_asc().using(&db).gets_before(&next.previous_cursor, 20).await?;
ts
const first = await Battle().orderBySeqAsc().using(db).getsAfter('', 20);
const next = await Battle().orderBySeqAsc().using(db).getsAfter(first.nextCursor, 20);
const previous = await Battle().orderBySeqAsc().using(db).getsBefore(next.previousCursor, 20);

An invalid version, order, value type, expression order, nullable order column, duplicate order column, or cursor order mismatch returns CURSOR_INVALID before SQL execution. Keyset pagination is a root query operation; joins and relations are preserved in the fetched page but cannot define the cursor order.

2. Tokens

2.1 Predicates <col><Op>(value) — WHERE

TokenSQLExample
<col> / <col>Eq=isClose(false)
<col>NotEq!=statusNotEq("x")
<col>Gt, Gte, Lt, LtecomparisonendDtGt(now)
<col>In, NotInIN, NOT INseqIn([1, 2, 3])
<col>Like, LikeBinaryLIKEnameLike("%kw%")
<col>Contains, StartsWith, EndsWithescaped LIKE patternnameContains(keyword)
<col>BetweenBETWEENcreatedTsBetween(from, to)
<col>IsNull, IsNotNullIS NULL, IS NOT NULLendDtIsNull()
<A>With<B>Matchfull-text matchnameWithDescriptionMatch(keyword)
<col><Op>Col(ref)column comparisonlangIdEqCol(ProductCols::langId())
expr(fragment, binds)schema-checked SQL fragmentexpr('DAYOFWEEK(created_ts) = ?', [1])
named predicateschema-defined predicate groupvisible()

An empty IN list returns EMPTY_IN. The allowed operators depend on the column type. The Eq suffix is the explicit equality method.

2.2 Groups and navigation — <X>Where

TokenMeaning
or()Connect the next item with OR. The default connection is AND.
and(fn) / or(fn)Add a nested group.
<rel>(fn)Add conditions to a declared joined relation. An undeclared path returns ENTITY_NOT_JOINED.
has<Rel>(fn) / notHas<Rel>(fn)Add an EXISTS or NOT EXISTS predicate for a declared relation. The predicate uses relation key mapping and target filters without requiring a join.
count<Rel><Op>(value, fn)Compare matching related row counts with eq, notEq, gt, gte, lt, or lte. The predicate uses a correlated count subquery and does not require a join.
on(fn) / where(fn)Use the same Where builder for join ON and WHERE conditions.

A leading or() or or(fn) returns OR_AT_GROUP_START. Two connectors without a predicate return DANGLING_CONNECTOR. Groups can be nested without a fixed depth.

2.3 Columns — SELECT

TokenMeaning
defaultSelect eager columns. Lazy and styled columns are excluded.
selectAll()Select all columns.
selectNone()Select primary and foreign keys.
select<Col>() / unselect<Col>()Add or remove one column.
select<Col>As(name)Select one column with an output name.
selectExpr(name, fragment)Select a schema-checked expression.

Output mapping is positional and uses {alias, column, out_name, index}. Join columns remain in their alias namespace.

2.4 Relations and joins

relation<Rel>(child) loads one related row. relations<Rel>(child) loads a collection. join<Rel>(child) and leftJoin<Rel>(child) add a SQL join. Relation kind and key mapping come from the schema.

Join conditions use child.on(fn) for ON and child.where(fn) for WHERE. A join may contain another declared join. The generated names are the same logical names in all clients.

2.5 Ordering, range, and options

Use orderBy<Col>Asc, orderBy<Col>Desc, groupBy<Col>, groupByExpr, having(fn), limit(offset, count), keyBy<Col>, keyByFn, flatten, and limitPerParent. parentNode is available only for the declared result merge operation.

2.6 Execution terminals

TerminalReturn
get()one row or null
gets()collection of rows
getCount()integer count
getsCount()grouped count rows
paginate(page, perPage)page result
`getBy<PKUnique>(value)`
getsBy<Col>(value)collection of rows
getCountBy<Col>(value)integer count

Finder methods apply their equality predicate and call the corresponding terminal. The database or executor is never passed to a terminal.

2.7 Batch writes

Batch writes use typed query drafts and one transaction. batchInsert, batchUpsert, batchUpdate, and batchDelete accept an entity-specific query list and a positive chunk size. The result contains attempted, affected, and inserted. For insert and upsert, affected is one per successful request because database drivers report different duplicate-update counts; inserted is the number of successful insert requests. For update and delete, affected is the driver-reported row count. An error rolls the complete batch back; a supplied transaction is reused.

2.8 Rows

A row has a generated getter and setter for each stored field. A setter marks the field dirty. Update operations use the row binding and preserve the original values required by optimistic locking. Relation accessors return the declared row or collection type.

3. Example: root and join conditions

php
$battles = Battle::query()
    ->using($db)
    ->serviceSeq(7)->isClose(false)
    ->and(fn($w) => $w->isDisplay(true)->or(fn($w) => $w->isAllday(true)))
    ->leftJoinUser(User::query()->on(fn($w) => $w->seqEqCol(BattleCols::userSeq())))
    ->orderBySeqDesc()->limit(0, 20)->gets();

The root group and join ON group are stored separately and compiled in declaration order.

4. Example: relations and parent limits

go
battles, err := gen.Battle().Using(ctx, db).
    ServiceSeq(7).IsClose(false).
    WithUser(gen.User().OrderBySeqDesc()).
    LimitPerParent(20).Gets()

A relation stage receives parent keys from the preceding stage. limitPerParent applies the limit within each parent key.

5. Example: writes and transactions

rust
let mut row = battle::query().using(&db).get_by_seq(7).await?.unwrap();
row.set_name("updated".to_owned());
row.update().await?;

Rows and queries use the same root transaction binding. A finished transaction rejects later operations with CONFIG.

6. Deliberately unsupported syntax

The regular API does not generate relationUser, relationsUser, matchAWithB, aliasName, or<Op><Col>, bind(db), database arguments on terminals, or new Battle-style language-specific entry points. Generated entry points use the language's constructor or factory form while preserving the same query structure.

Styled columns

Styles are schema declarations. aes, hex, gz, json, jsons, base64, and serialize are applied by the host codec or dialect as defined in codec.md. aes_key_version is plaintext metadata, has no style, and is excluded from the default projection.

MIT License · Go / PHP / Rust / TypeScript