From 1df4a4f662c37585bf6b1e66c6b3f98489c67a69 Mon Sep 17 00:00:00 2001 From: Dmitry Kurochkin Date: Mon, 25 May 2026 11:49:39 +0000 Subject: [PATCH 1/2] Add per-row arithmetic and date/datetime math operators Closes the gap that made per-row computed columns inexpressible. Adds to the each* family established in 0.2.0: - eachAdd / eachSubtract / eachMultiply / eachDivide (numeric) values items accept numericReturning or numericArrayReturning, result is numericArrayReturning; usable in select, aggregate args, and right operand of each* comparisons. - eachDateAddDays / eachDateDiffDays (days as the unit) - eachDatetimeAddSeconds / eachDatetimeDiffSeconds (seconds as the unit) Larger date/datetime units are obtained by composition with eachMultiply (e.g. add N hours = eachDatetimeAddSeconds(dt, eachMultiply(n, 3600))). No interval type or per-unit operator family is introduced. CLAUDE.md rewritten around the "two operator families" model; previously-outdated rules about fields in arithmetic/comparison are replaced with a where-each-family-fits table. README extended with per-operator subsections and four new samples covering computed columns, sum-of-product, date offsets, and datetime-diff filtering. Version bump 0.1.0-preview.0.2.0 -> 0.1.0-preview.0.3.0. Refs #47. --- CHANGELOG.md | 45 ++++ CLAUDE.md | 64 ++++- PureQL-Specification.json | 277 ++++++++++++++++++++- README.md | 90 ++++++- samples/17_each_arithmetic_select.json | 16 ++ samples/18_aggregate_of_each_multiply.json | 31 +++ samples/19_each_date_add_days.json | 13 + samples/20_each_datetime_diff_where.json | 17 ++ 8 files changed, 529 insertions(+), 24 deletions(-) create mode 100644 samples/17_each_arithmetic_select.json create mode 100644 samples/18_aggregate_of_each_multiply.json create mode 100644 samples/19_each_date_add_days.json create mode 100644 samples/20_each_datetime_diff_where.json diff --git a/CHANGELOG.md b/CHANGELOG.md index b74f07b..a9ef79b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,51 @@ Versioning follows Semantic Versioning with preview suffix `major.minor.patch-pr --- +## [0.1.0-preview.0.3.0] - 2026-05-25 + +Adds the per-row arithmetic and date/datetime math operators to the `each*` family established in `0.2.0`. Together with the existing per-row boolean/comparison operators, this closes the gap that previously made per-row computed columns inexpressible (e.g. `unit_price * quantity AS subtotal`, `sum(unit_price * quantity)`, `order_date + 30 days`, `shipped_at - ordered_at > 48h`). Purely additive on top of `0.2.0`. + +Tracking issue: #47. + +### Added + +- **Per-row numeric arithmetic**: `eachAdd`, `eachSubtract`, `eachMultiply`, `eachDivide`. `values` is an array of `numericReturning | numericArrayReturning` items (broadcast scalars, zip arrays), `minItems: 2`. Result is `numericArrayReturning`. Mirrors the existing single-value `arithmetic` family but accepts fields directly. Slots into `select` (computed columns), aggregate `arg`, and the right operand of any `each*` comparison. Added to `numericArrayReturning`. +- **Per-row date math**: + - `eachDateAddDays` — `{ left: date \| dateArray, right: number \| numberArray }` → `dateArrayReturning`. Adds N days per row. + - `eachDateDiffDays` — `{ left: date \| dateArray, right: date \| dateArray }` → `numericArrayReturning`. Difference in days. +- **Per-row datetime math**: + - `eachDatetimeAddSeconds` — `{ left: datetime \| datetimeArray, right: number \| numberArray }` → `dateTimeArrayReturning`. Adds N seconds per row. + - `eachDatetimeDiffSeconds` — `{ left: datetime \| datetimeArray, right: datetime \| datetimeArray }` → `numericArrayReturning`. Difference in seconds. +- New schema groups `eachArithmetics`, `eachDateArithmetics`, `eachDatetimeArithmetics`, plus the `eachArithmetic` union. The two diff operators are referenced from `numericArrayReturning`; `eachDateAddDays` from `dateArrayReturning`; `eachDatetimeAddSeconds` from `dateTimeArrayReturning`. +- Four new reference samples: + - `17_each_arithmetic_select.json` — `eachMultiply(unit_price, quantity)` as a computed `select` column. + - `18_aggregate_of_each_multiply.json` — `sum(eachMultiply(unit_price, quantity))` grouped by user — the textbook line-item revenue query. + - `19_each_date_add_days.json` — `eachDateAddDays(order_date, 30)` derives a `delivery_eta` column. + - `20_each_datetime_diff_where.json` — `eachDatetimeDiffSeconds` inside `eachGreaterThan` filters orders whose ship time exceeds 48 hours. + +### Changed + +- **`CLAUDE.md` rewritten around the "two operator families" model.** The old "fields cannot appear in arithmetic / comparison" rules — outdated since `0.2.0` and now wrong for arithmetic too — were replaced with explicit guidance on when to use each family. A new "Where each family fits" table maps clauses to accepted families. The "Field equality uses `arrayEquality`" rule is now "prefer `eachEqual`; `arrayEquality` is reserved for whole-sequence equality". +- **README "Operations" section** extended with per-operator subsections for `eachAdd`/`eachSubtract`/`eachMultiply`/`eachDivide`, `eachDateAddDays`/`eachDateDiffDays`, `eachDatetimeAddSeconds`/`eachDatetimeDiffSeconds`. The "Two families" overview now lists arithmetic alongside booleans/comparisons. + +### Interpreter notes + +- **Result type of per-row arithmetic**: every `eachX` arithmetic operator returns a numeric/date/datetime *column* aligned with the current row set. Maps to SQL projection expressions, LINQ row lambdas, or in-memory column transforms — same model as `0.2.0`'s `each*` comparisons. +- **Operand polymorphism**: `values[i]` (for `eachAdd`/`Subtract`/`Multiply`/`Divide`) and `left` / `right` (for date/datetime math) accept `*Returning` (one value broadcast to every row) or `*ArrayReturning` (zipped element-wise). Dispatch on which `oneOf` arm matched; in column-evaluator backends this is the broadcast-vs-zip distinction. +- **Subtract / divide ordering**: same as their single-value siblings — left-to-right fold over the `values` array. +- **Aggregate over per-row arithmetic**: `sum`/`min_*`/`max_*`/`average_*` already accepted `*ArrayReturning` as `arg`; with `eachX` now in that union, the same dispatch covers expressions like `sum(eachMultiply(field_a, field_b))` without additional cases. +- **Unit choice for date/datetime math**: days for `date`, seconds for `datetime`. Larger units are obtained by composition — e.g. "add N hours to a datetime" is `eachDatetimeAddSeconds(dt, eachMultiply(n, 3600))`. There is intentionally no `interval` type and no per-unit operator family; interpreters should not emulate one. Negative `right` values produce subtraction. +- **`eachDateDiffDays` / `eachDatetimeDiffSeconds` semantics**: `left - right`, so a positive result means `left` is later. Document or normalize this in the host backend; the schema fixes the convention. +- **Division by zero / overflow / null propagation**: schema doesn't constrain. Define per-backend. +- **No `each*` arithmetic on `time` or `string`**: out of scope for this release; addable later without breakage. +- **CLAUDE.md interpreter rule**: any backend that previously rejected fields under `add`/`multiply`/etc. should keep rejecting them there — the *single-value* arithmetic family is unchanged. Fields now flow exclusively through the per-row family. + +### Versioning + +Minor preview bump (`0.1.0-preview.0.2.0` → `0.1.0-preview.0.3.0`). All schema changes are additive; no existing operator or shape was renamed, removed, or tightened. Both `version` and `$id` updated. + +--- + ## [0.1.0-preview.0.2.0] - 2026-05-25 This release establishes the **per-row predicate family (`each*`)** as a first-class, type-distinct set of expressions. All `each*` operators now return a per-row boolean *column* (`booleanArrayReturning`), not a single boolean — making PureQL's type system express the row-vs-scalar distinction that SQL hides. This is purely additive on top of `0.1.0-preview.0.1.0`: no operator existing in that release was removed or renamed in a user-visible way. diff --git a/CLAUDE.md b/CLAUDE.md index d0ab485..2e54f47 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,33 +32,71 @@ print('valid') ## Critical design rules (read before editing samples) -### Fields live in `arrayReturning`, not `singleValueReturning` +### Two predicate / expression families -A field reference (`{ entity, field, type }`) is an **array-returning** expression — it represents a whole column. Consequently: +Every operator in the schema belongs to one of two parallel families: -- Fields go in `select`, `groupBy`, `orderBy`, `join.on`, and as `arg` to aggregate functions. -- Fields **cannot** appear directly as operands of `arithmetic` (`add`, `multiply`, etc.) — use an aggregate like `sum` to reduce them first. -- Fields **cannot** appear as operands of `comparison` (`greaterThan`, etc.) — those operators accept `numericReturning` / `stringReturning` (scalars, aggregates, arithmetic), not field references. +- **Single-value family** (`and`, `or`, `not`, `equal`, `greaterThan`, …, `add`, `subtract`, `multiply`, `divide`) — operands and result are single values. Use when both sides reduce to one value per query (or per group, inside `having`): typically scalars, parameters, aggregates, or arithmetic over them. +- **Per-row (`each*`) family** (`eachAnd`, `eachOr`, `eachNot`, `eachEqual`, `eachGreaterThan`, …, `eachAdd`, `eachMultiply`, `eachDateAddDays`, `eachDatetimeDiffSeconds`, …) — operate per row of the current row set; result is a vector aligned with the input rows. Use when at least one operand is a field, or to build computed per-row columns. -### Field equality uses `arrayEquality` +**Do not mix families inside the same boolean operator.** `and`/`or`/`not` take only single-boolean children; `eachAnd`/`eachOr`/`eachNot` take only per-row boolean children. -Comparing a field to a literal requires the literal to be an **array scalar**: +### Where each family fits + +| Clause | Accepts | +|---|---| +| `where` | single-value boolean **or** per-row boolean (per-row is the common case) | +| `join.on` | single-value boolean **or** per-row boolean (per-row equi-join is the common case) | +| `having` | single-value boolean **only** — operands must reduce to one value per group | +| `select` | any value-returning expression, including per-row computed columns | +| `groupBy` / `orderBy` | field references only | + +### Fields are `arrayReturning` + +A field reference (`{ entity, field, type }`) is an **array-returning** expression — it represents a whole column. Fields: + +- Go in `select`, `groupBy`, `orderBy`, as `arg` to aggregate functions, and as operands of any `each*` operator. +- **Cannot** appear directly in single-value `add`/`multiply`/`greaterThan`/`equal`/etc. — use the per-row `eachX` variant for the row context, or reduce with an aggregate (`sum`, `count`, `max_*`, …) for the single-value context. + +### Field equality: prefer `eachEqual` over `arrayEquality` + +For "field = literal" or "field = field" filtering, use **`eachEqual`** with an unwrapped scalar on the right: ```json { - "operator": "equal", + "operator": "eachEqual", "left": { "entity": "users", "field": "status", "type": { "name": "string" } }, - "right": { "type": { "name": "stringArray" }, "value": ["active"] } + "right": { "type": { "name": "string" }, "value": "active" } } ``` -The type on the right is `stringArray`, not `string`. This is `string_array_equality` under `arrayEquality`. +`arrayEquality` (the `equal` operator with `*ArrayReturning` on both sides) still validates and is semantically distinct — it asks "are these two **whole sequences** equal as wholes?" and returns one boolean. Reserve it for that intent (e.g. comparing two parameter arrays). The historical idiom of `equal(field, [singleValue])` as a per-row filter has been migrated out of all bundled samples. + +### Per-row equality vs single-value equality + +| Operator | Operands | Result | Typical placement | +|---|---|---|---| +| `equal` (single-value) | two `*Returning` | one boolean | `having` against aggregates | +| `equal` (whole-array) | two `*ArrayReturning` | one boolean | rare — whole-sequence equality | +| `eachEqual` | `*ArrayReturning` left, `*Returning` or `*ArrayReturning` right | one boolean per row | `where`, `join.on` | + +### Range comparisons + +Two parallel sets, same convention: + +- Single-value: `greaterThan` / `lessThan` / `greaterThanOrEqual` / `lessThanOrEqual` over `numericReturning` / `stringReturning` / `dateReturning` / etc. Use in `having`. +- Per-row: `eachGreaterThan` / `eachLessThan` / `eachGreaterThanOrEqual` / `eachLessThanOrEqual`. `left` is `*ArrayReturning` (typically a field); `right` is `*Returning` (broadcast scalar) **or** `*ArrayReturning` (element-wise other field). Use in `where` / `join.on`. + +### Arithmetic and date math -`singleValueEquality` (with `stringReturning` / `numericReturning`) is for comparing aggregates or scalars to each other, not for field comparisons. +Same convention: -### Range comparisons only on single-value expressions +- Single-value `add` / `subtract` / `multiply` / `divide` — `values` items are `numericReturning` only. Use to combine aggregates and constants (e.g. `multiply(sum(total), 0.05)`). +- Per-row `eachAdd` / `eachSubtract` / `eachMultiply` / `eachDivide` — `values` items are `numericReturning | numericArrayReturning`. Use for computed per-row columns (e.g. `eachMultiply(unit_price field, quantity field)`). +- Date math: `eachDateAddDays(date, n_days) → date`, `eachDateDiffDays(date1, date2) → number`. +- Datetime math: `eachDatetimeAddSeconds(datetime, n_seconds) → datetime`, `eachDatetimeDiffSeconds(dt1, dt2) → number`. -`greaterThan` / `lessThan` etc. operate on `numericReturning` / `stringReturning` / `dateReturning` / etc. — scalars, parameters, and aggregates only. Use them in `having` to filter groups by aggregate results. +Larger date/time units are expressed via composition with `eachMultiply` (e.g. `eachDatetimeAddSeconds(dt, eachMultiply(days, 86400))`). No `interval` type exists. ### `joinItem` has no alias diff --git a/PureQL-Specification.json b/PureQL-Specification.json index 846823d..4a32aa3 100644 --- a/PureQL-Specification.json +++ b/PureQL-Specification.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/kudima03/PureQL-Specification/releases/download/0.1.0-preview.0.2.0/PureQL-Specification.json", - "version": "0.1.0-preview.0.2.0", + "$id": "https://github.com/kudima03/PureQL-Specification/releases/download/0.1.0-preview.0.3.0/PureQL-Specification.json", + "version": "0.1.0-preview.0.3.0", "definitions": { "arrayParameters": { "stringArrayParameter": { @@ -513,6 +513,15 @@ }, { "$ref": "#/definitions/arrayParameters/numberArrayParameter" + }, + { + "$ref": "#/definitions/eachArithmetic" + }, + { + "$ref": "#/definitions/eachDateArithmetics/eachDateDiffDays" + }, + { + "$ref": "#/definitions/eachDatetimeArithmetics/eachDatetimeDiffSeconds" } ] }, @@ -539,6 +548,9 @@ }, { "$ref": "#/definitions/arrayParameters/dateArrayParameter" + }, + { + "$ref": "#/definitions/eachDateArithmetics/eachDateAddDays" } ] }, @@ -565,6 +577,9 @@ }, { "$ref": "#/definitions/arrayParameters/datetimeArrayParameter" + }, + { + "$ref": "#/definitions/eachDatetimeArithmetics/eachDatetimeAddSeconds" } ] }, @@ -1011,6 +1026,264 @@ ] } }, + "eachArithmetic": { + "oneOf": [ + { + "$ref": "#/definitions/eachArithmetics/eachAdd" + }, + { + "$ref": "#/definitions/eachArithmetics/eachSubtract" + }, + { + "$ref": "#/definitions/eachArithmetics/eachMultiply" + }, + { + "$ref": "#/definitions/eachArithmetics/eachDivide" + } + ] + }, + "eachArithmetics": { + "eachAdd": { + "type": "object", + "required": [ + "operator", + "values" + ], + "properties": { + "operator": { + "const": "eachAdd" + }, + "values": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/numericReturning" + }, + { + "$ref": "#/definitions/numericArrayReturning" + } + ] + }, + "minItems": 2 + } + } + }, + "eachSubtract": { + "type": "object", + "required": [ + "operator", + "values" + ], + "properties": { + "operator": { + "const": "eachSubtract" + }, + "values": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/numericReturning" + }, + { + "$ref": "#/definitions/numericArrayReturning" + } + ] + }, + "minItems": 2 + } + } + }, + "eachMultiply": { + "type": "object", + "required": [ + "operator", + "values" + ], + "properties": { + "operator": { + "const": "eachMultiply" + }, + "values": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/numericReturning" + }, + { + "$ref": "#/definitions/numericArrayReturning" + } + ] + }, + "minItems": 2 + } + } + }, + "eachDivide": { + "type": "object", + "required": [ + "operator", + "values" + ], + "properties": { + "operator": { + "const": "eachDivide" + }, + "values": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/numericReturning" + }, + { + "$ref": "#/definitions/numericArrayReturning" + } + ] + }, + "minItems": 2 + } + } + } + }, + "eachDateArithmetics": { + "eachDateAddDays": { + "type": "object", + "required": [ + "operator", + "left", + "right" + ], + "properties": { + "operator": { + "const": "eachDateAddDays" + }, + "left": { + "oneOf": [ + { + "$ref": "#/definitions/dateReturning" + }, + { + "$ref": "#/definitions/dateArrayReturning" + } + ] + }, + "right": { + "oneOf": [ + { + "$ref": "#/definitions/numericReturning" + }, + { + "$ref": "#/definitions/numericArrayReturning" + } + ] + } + } + }, + "eachDateDiffDays": { + "type": "object", + "required": [ + "operator", + "left", + "right" + ], + "properties": { + "operator": { + "const": "eachDateDiffDays" + }, + "left": { + "oneOf": [ + { + "$ref": "#/definitions/dateReturning" + }, + { + "$ref": "#/definitions/dateArrayReturning" + } + ] + }, + "right": { + "oneOf": [ + { + "$ref": "#/definitions/dateReturning" + }, + { + "$ref": "#/definitions/dateArrayReturning" + } + ] + } + } + } + }, + "eachDatetimeArithmetics": { + "eachDatetimeAddSeconds": { + "type": "object", + "required": [ + "operator", + "left", + "right" + ], + "properties": { + "operator": { + "const": "eachDatetimeAddSeconds" + }, + "left": { + "oneOf": [ + { + "$ref": "#/definitions/dateTimeReturning" + }, + { + "$ref": "#/definitions/dateTimeArrayReturning" + } + ] + }, + "right": { + "oneOf": [ + { + "$ref": "#/definitions/numericReturning" + }, + { + "$ref": "#/definitions/numericArrayReturning" + } + ] + } + } + }, + "eachDatetimeDiffSeconds": { + "type": "object", + "required": [ + "operator", + "left", + "right" + ], + "properties": { + "operator": { + "const": "eachDatetimeDiffSeconds" + }, + "left": { + "oneOf": [ + { + "$ref": "#/definitions/dateTimeReturning" + }, + { + "$ref": "#/definitions/dateTimeArrayReturning" + } + ] + }, + "right": { + "oneOf": [ + { + "$ref": "#/definitions/dateTimeReturning" + }, + { + "$ref": "#/definitions/dateTimeArrayReturning" + } + ] + } + } + } + }, "booleanOperations": { "or": { "type": "object", diff --git a/README.md b/README.md index 90a7f7a..e43bf5b 100644 --- a/README.md +++ b/README.md @@ -163,16 +163,26 @@ Both accept an array of field references. ## Operations -### Two families of predicates +### Two operator families -PureQL has **two parallel predicate families** that you choose between based on whether you're working with single values or with columns/rows: +Every operator in the schema belongs to one of two parallel families that you choose between based on whether you're working with single values or with columns/rows: -| Need | Family | Returns | Operators | +| Family | Operands | Result | Operators | |---|---|---|---| -| Combine/compare **single-value** expressions (aggregates, scalars) | Single-value family | one boolean | `and`, `or`, `not`, `equal`, `greaterThan`, `lessThan`, … | -| Filter/compare values **per row** of a column | Per-row (`each*`) family | one boolean **per row** | `eachAnd`, `eachOr`, `eachNot`, `eachEqual`, `eachGreaterThan`, `eachLessThan`, … | +| **Single-value** | reduce to one value per query (or per group) | one value | `and`, `or`, `not`, `equal`, `greaterThan`/`lessThan`/…, `add`/`subtract`/`multiply`/`divide`, aggregates | +| **Per-row (`each*`)** | at least one operand is a field or per-row computed column | one value **per row** | `eachAnd`/`eachOr`/`eachNot`, `eachEqual`, `eachGreaterThan`/`eachLessThan`/…, `eachAdd`/`eachSubtract`/`eachMultiply`/`eachDivide`, `eachDateAddDays`/`eachDateDiffDays`, `eachDatetimeAddSeconds`/`eachDatetimeDiffSeconds` | -`where` and `join.on` accept either family (they evaluate per row, but a literal `true` is also valid). `having` accepts only the single-value family. `and` / `or` / `not` themselves only accept single-value children — do not mix the two families inside the same boolean operator; use `eachAnd` / `eachOr` / `eachNot` to compose per-row predicates. +Where each family fits: + +| Clause | Accepts | +|---|---| +| `where` / `join.on` | per-row boolean (typical) or single-value boolean | +| `having` | single-value boolean only — non-aggregated fields are structurally rejected | +| `select` | any value-returning expression, including per-row computed columns | +| `sum.arg` / `min_*.arg` / `max_*.arg` / `average_*.arg` | any array-returning expression (field, per-row computation) | +| right operand of any `each*` comparison | matching `*Returning` (broadcast scalar) or `*ArrayReturning` (element-wise) | + +**Do not mix families inside the same boolean operator.** `and`/`or`/`not` accept only single-value boolean children; `eachAnd`/`eachOr`/`eachNot` accept only per-row boolean children. The schema enforces this via type. ### Single-value boolean operations @@ -309,9 +319,9 @@ Field vs field (per-row): } ``` -### Arithmetic operators +### Single-value arithmetic -Arithmetic works on **numeric single-value-returning** expressions. Use aggregates to bridge field data into arithmetic. +Combines **single-value-returning** numeric expressions. Use aggregates to bridge field data into single-value arithmetic. | Operator | Meaning | |-------------|---------| @@ -320,7 +330,7 @@ Arithmetic works on **numeric single-value-returning** expressions. Use aggregat | `multiply` | `*` | | `divide` | `/` | -All operators take a `values` array with at least 2 operands. +`values` is an array with at least 2 operands. For `subtract` and `divide`, evaluation is left-to-right (`[a, b, c]` means `a - b - c` / `a / b / c`). ```json { @@ -333,6 +343,64 @@ All operators take a `values` array with at least 2 operands. } ``` +### Per-row arithmetic (`eachAdd` / `eachSubtract` / `eachMultiply` / `eachDivide`) + +Per-row analogues of arithmetic. Each `values[i]` is `numericReturning` (broadcast scalar) or `numericArrayReturning` (zipped per-row column). Result is a numeric column aligned with the row set. + +Use in `select` for computed columns, inside aggregates (`sum(eachMultiply(unit_price, quantity))`), or on the right side of any `each*` comparison. + +```json +{ + "operator": "eachMultiply", + "values": [ + { "entity": "order_items", "field": "unit_price", "type": { "name": "number" } }, + { "entity": "order_items", "field": "quantity", "type": { "name": "number" } } + ], + "alias": "subtotal" +} +``` + +### Date math (`eachDateAddDays` / `eachDateDiffDays`) + +Per-row date arithmetic, days as the unit. + +| Operator | Shape | Returns | +|---|---|---| +| `eachDateAddDays` | `{ left: date, right: number }` | `date` (per row) | +| `eachDateDiffDays` | `{ left: date, right: date }` | `number` (per row) | + +`left` / `right` accept the broadcast vs zipped polymorphism (`*Returning | *ArrayReturning`). + +```json +{ + "operator": "eachDateAddDays", + "left": { "entity": "orders", "field": "order_date", "type": { "name": "date" } }, + "right": { "type": { "name": "number" }, "value": 30 }, + "alias": "delivery_eta" +} +``` + +### Datetime math (`eachDatetimeAddSeconds` / `eachDatetimeDiffSeconds`) + +Per-row datetime arithmetic, seconds as the unit. Larger units are expressed by composing with `eachMultiply` (e.g. add `N` hours via `eachDatetimeAddSeconds(dt, eachMultiply(n, 3600))`). + +| Operator | Shape | Returns | +|---|---|---| +| `eachDatetimeAddSeconds` | `{ left: datetime, right: number }` | `datetime` (per row) | +| `eachDatetimeDiffSeconds` | `{ left: datetime, right: datetime }` | `number` (per row) | + +```json +{ + "operator": "eachGreaterThan", + "left": { + "operator": "eachDatetimeDiffSeconds", + "left": { "entity": "orders", "field": "shipped_at", "type": { "name": "datetime" } }, + "right": { "entity": "orders", "field": "ordered_at", "type": { "name": "datetime" } } + }, + "right": { "type": { "name": "number" }, "value": 172800 } +} +``` + ### Aggregates Aggregates reduce an array of field values to a single value. The `arg` is any array-returning expression (typically a field reference). @@ -386,3 +454,7 @@ The [`samples/`](samples/) directory contains query examples ordered by complexi | [`14_each_field_to_field.json`](samples/14_each_field_to_field.json) | Per-row range comparison between two fields (no scalar threshold) | | [`15_each_not_equal.json`](samples/15_each_not_equal.json) | `eachNot` wrapping `eachEqual` — the idiom for "field ≠ literal" | | [`16_each_or_composition.json`](samples/16_each_or_composition.json) | Mixing `eachEqual` and `eachGreaterThan` via `eachAnd` + `eachOr` | +| [`17_each_arithmetic_select.json`](samples/17_each_arithmetic_select.json) | `eachMultiply` of two fields as a computed `select` column (`subtotal`) | +| [`18_aggregate_of_each_multiply.json`](samples/18_aggregate_of_each_multiply.json) | `sum(eachMultiply(unit_price, quantity))` grouped by user — line-item revenue | +| [`19_each_date_add_days.json`](samples/19_each_date_add_days.json) | `eachDateAddDays` to derive a `delivery_eta` column from `order_date + 30 days` | +| [`20_each_datetime_diff_where.json`](samples/20_each_datetime_diff_where.json) | `eachDatetimeDiffSeconds` inside `eachGreaterThan` to filter orders by ship-time | diff --git a/samples/17_each_arithmetic_select.json b/samples/17_each_arithmetic_select.json new file mode 100644 index 0000000..b706235 --- /dev/null +++ b/samples/17_each_arithmetic_select.json @@ -0,0 +1,16 @@ +{ + "from": { "entity": "order_items" }, + "select": [ + { "entity": "order_items", "field": "id", "type": { "name": "uuid" } }, + { "entity": "order_items", "field": "unit_price", "type": { "name": "number" } }, + { "entity": "order_items", "field": "quantity", "type": { "name": "number" } }, + { + "operator": "eachMultiply", + "values": [ + { "entity": "order_items", "field": "unit_price", "type": { "name": "number" } }, + { "entity": "order_items", "field": "quantity", "type": { "name": "number" } } + ], + "alias": "subtotal" + } + ] +} diff --git a/samples/18_aggregate_of_each_multiply.json b/samples/18_aggregate_of_each_multiply.json new file mode 100644 index 0000000..3b6acfb --- /dev/null +++ b/samples/18_aggregate_of_each_multiply.json @@ -0,0 +1,31 @@ +{ + "from": { "entity": "order_items", "alias": "oi" }, + "select": [ + { "entity": "orders", "field": "user_id", "type": { "name": "uuid" } }, + { + "operator": "sum", + "arg": { + "operator": "eachMultiply", + "values": [ + { "entity": "oi", "field": "unit_price", "type": { "name": "number" } }, + { "entity": "oi", "field": "quantity", "type": { "name": "number" } } + ] + }, + "alias": "line_item_revenue" + } + ], + "joins": [ + { + "type": "inner", + "entity": "orders", + "on": { + "operator": "eachEqual", + "left": { "entity": "oi", "field": "order_id", "type": { "name": "uuid" } }, + "right": { "entity": "orders", "field": "id", "type": { "name": "uuid" } } + } + } + ], + "groupBy": [ + { "entity": "orders", "field": "user_id", "type": { "name": "uuid" } } + ] +} diff --git a/samples/19_each_date_add_days.json b/samples/19_each_date_add_days.json new file mode 100644 index 0000000..9426d78 --- /dev/null +++ b/samples/19_each_date_add_days.json @@ -0,0 +1,13 @@ +{ + "from": { "entity": "orders" }, + "select": [ + { "entity": "orders", "field": "id", "type": { "name": "uuid" } }, + { "entity": "orders", "field": "order_date", "type": { "name": "date" } }, + { + "operator": "eachDateAddDays", + "left": { "entity": "orders", "field": "order_date", "type": { "name": "date" } }, + "right": { "type": { "name": "number" }, "value": 30 }, + "alias": "delivery_eta" + } + ] +} diff --git a/samples/20_each_datetime_diff_where.json b/samples/20_each_datetime_diff_where.json new file mode 100644 index 0000000..f07c907 --- /dev/null +++ b/samples/20_each_datetime_diff_where.json @@ -0,0 +1,17 @@ +{ + "from": { "entity": "orders" }, + "select": [ + { "entity": "orders", "field": "id", "type": { "name": "uuid" } }, + { "entity": "orders", "field": "ordered_at", "type": { "name": "datetime" } }, + { "entity": "orders", "field": "shipped_at", "type": { "name": "datetime" } } + ], + "where": { + "operator": "eachGreaterThan", + "left": { + "operator": "eachDatetimeDiffSeconds", + "left": { "entity": "orders", "field": "shipped_at", "type": { "name": "datetime" } }, + "right": { "entity": "orders", "field": "ordered_at", "type": { "name": "datetime" } } + }, + "right": { "type": { "name": "number" }, "value": 172800 } + } +} From 3f6be7c0233b0c52e358fc61b7425ac53243bfa4 Mon Sep 17 00:00:00 2001 From: Dmitry Kurochkin Date: Mon, 25 May 2026 12:25:36 +0000 Subject: [PATCH 2/2] Add eachTimeAddSeconds/eachTimeDiffSeconds and document broadcast/zip semantics Mirrors the date/datetime math operators for the time type with seconds as the unit, restoring symmetry across all temporal types. eachTimeAddSeconds returns timeArrayReturning; eachTimeDiffSeconds returns numericArrayReturning. Wrap-around behaviour at the day boundary is deliberately interpreter- defined. Adds a dedicated "Broadcast vs zip" subsection to CLAUDE.md, README, and CHANGELOG interpreter notes. Explains that any each* slot accepting both *Returning and *ArrayReturning fixes a row count N from the surrounding context, broadcasts scalars to length N, zips arrays element-wise, then applies the operator. Key point: the result is *ArrayReturning even when every operand is scalar (eachAdd(2, 3) over 4 rows yields [5, 5, 5, 5]), which is why eachX cannot be optimized into its single-value sibling. Sample 21 demonstrates eachTimeAddSeconds and eachTimeDiffSeconds over clock_in / clock_out time fields. --- CHANGELOG.md | 40 +++++++++++++----- CLAUDE.md | 18 ++++++++- PureQL-Specification.json | 74 ++++++++++++++++++++++++++++++++++ README.md | 35 +++++++++++++++- samples/21_each_time_math.json | 20 +++++++++ 5 files changed, 173 insertions(+), 14 deletions(-) create mode 100644 samples/21_each_time_math.json diff --git a/CHANGELOG.md b/CHANGELOG.md index a9ef79b..fece160 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,12 +22,16 @@ Tracking issue: #47. - **Per-row datetime math**: - `eachDatetimeAddSeconds` — `{ left: datetime \| datetimeArray, right: number \| numberArray }` → `dateTimeArrayReturning`. Adds N seconds per row. - `eachDatetimeDiffSeconds` — `{ left: datetime \| datetimeArray, right: datetime \| datetimeArray }` → `numericArrayReturning`. Difference in seconds. -- New schema groups `eachArithmetics`, `eachDateArithmetics`, `eachDatetimeArithmetics`, plus the `eachArithmetic` union. The two diff operators are referenced from `numericArrayReturning`; `eachDateAddDays` from `dateArrayReturning`; `eachDatetimeAddSeconds` from `dateTimeArrayReturning`. -- Four new reference samples: +- **Per-row time math**: + - `eachTimeAddSeconds` — `{ left: time \| timeArray, right: number \| numberArray }` → `timeArrayReturning`. Adds N seconds per row; wrap/saturate/error behaviour around `00:00:00` is interpreter-defined. + - `eachTimeDiffSeconds` — `{ left: time \| timeArray, right: time \| timeArray }` → `numericArrayReturning`. Difference in seconds. +- New schema groups `eachArithmetics`, `eachDateArithmetics`, `eachTimeArithmetics`, `eachDatetimeArithmetics`, plus the `eachArithmetic` union. The three diff operators are referenced from `numericArrayReturning`; `eachDateAddDays` from `dateArrayReturning`; `eachTimeAddSeconds` from `timeArrayReturning`; `eachDatetimeAddSeconds` from `dateTimeArrayReturning`. +- Five new reference samples: - `17_each_arithmetic_select.json` — `eachMultiply(unit_price, quantity)` as a computed `select` column. - `18_aggregate_of_each_multiply.json` — `sum(eachMultiply(unit_price, quantity))` grouped by user — the textbook line-item revenue query. - `19_each_date_add_days.json` — `eachDateAddDays(order_date, 30)` derives a `delivery_eta` column. - `20_each_datetime_diff_where.json` — `eachDatetimeDiffSeconds` inside `eachGreaterThan` filters orders whose ship time exceeds 48 hours. + - `21_each_time_math.json` — `eachTimeAddSeconds` and `eachTimeDiffSeconds` over `clock_in` / `clock_out` time fields. ### Changed @@ -36,15 +40,29 @@ Tracking issue: #47. ### Interpreter notes -- **Result type of per-row arithmetic**: every `eachX` arithmetic operator returns a numeric/date/datetime *column* aligned with the current row set. Maps to SQL projection expressions, LINQ row lambdas, or in-memory column transforms — same model as `0.2.0`'s `each*` comparisons. -- **Operand polymorphism**: `values[i]` (for `eachAdd`/`Subtract`/`Multiply`/`Divide`) and `left` / `right` (for date/datetime math) accept `*Returning` (one value broadcast to every row) or `*ArrayReturning` (zipped element-wise). Dispatch on which `oneOf` arm matched; in column-evaluator backends this is the broadcast-vs-zip distinction. -- **Subtract / divide ordering**: same as their single-value siblings — left-to-right fold over the `values` array. -- **Aggregate over per-row arithmetic**: `sum`/`min_*`/`max_*`/`average_*` already accepted `*ArrayReturning` as `arg`; with `eachX` now in that union, the same dispatch covers expressions like `sum(eachMultiply(field_a, field_b))` without additional cases. -- **Unit choice for date/datetime math**: days for `date`, seconds for `datetime`. Larger units are obtained by composition — e.g. "add N hours to a datetime" is `eachDatetimeAddSeconds(dt, eachMultiply(n, 3600))`. There is intentionally no `interval` type and no per-unit operator family; interpreters should not emulate one. Negative `right` values produce subtraction. -- **`eachDateDiffDays` / `eachDatetimeDiffSeconds` semantics**: `left - right`, so a positive result means `left` is later. Document or normalize this in the host backend; the schema fixes the convention. -- **Division by zero / overflow / null propagation**: schema doesn't constrain. Define per-backend. -- **No `each*` arithmetic on `time` or `string`**: out of scope for this release; addable later without breakage. -- **CLAUDE.md interpreter rule**: any backend that previously rejected fields under `add`/`multiply`/etc. should keep rejecting them there — the *single-value* arithmetic family is unchanged. Fields now flow exclusively through the per-row family. +- **Result type of per-row arithmetic**: every `eachX` arithmetic / date / time / datetime operator returns a numeric / date / time / datetime *column* aligned with the current row set. Maps to SQL projection expressions, LINQ row lambdas, or in-memory column transforms — same model as `0.2.0`'s `each*` comparisons. +- **Broadcast vs zip semantics (mixed `*Returning` and `*ArrayReturning` operands)**: every `each*` slot that admits both kinds uses one shared evaluation rule: + 1. The surrounding row set fixes a row count `N` — determined by `from` + `joins` + `where` for `where` / `select` / `join.on` expressions, or by the group size for expressions nested inside an aggregate `arg` after `groupBy`. + 2. Each `*Returning` operand is **broadcast** — repeated `N` times so it has one value per row. (In SQL backends, this is implicit — a scalar in a projection IS the value for every row.) + 3. Each `*ArrayReturning` operand is already aligned with the same `N` rows by construction (it comes from the same row set / group). + 4. The operator runs **element-wise across all operands**, producing a length-`N` result vector. + + So `eachAdd([fieldA, scalar, fieldB])` over 3 rows with `fieldA = [10, 20, 30]`, `scalar = 5`, `fieldB = [1, 2, 3]` evaluates to `[16, 27, 38]`. This is what makes `eachMultiply(unit_price, 1.05)` (5% per-row markup) and `eachAdd(base_price, tax, shipping)` (sum three columns per row) work in one place. + + The return type is always `*ArrayReturning` even if every operand is a `*Returning`: `eachAdd(2, 3)` in a `select` over a 4-row table yields `[5, 5, 5, 5]`, not `5`. Interpreters should not optimize this away as a constant — the row-count alignment is structural. Use single-value `add` for purely scalar work. + + Backend implementation hints: + - SQL: scalars become literals in the projection / `WHERE` predicate; `*ArrayReturning` operands become column references. The SQL engine handles broadcast natively. No special case needed. + - LINQ / row-lambda backends: emit `row => operator(arg1(row), arg2(row), …)` where each `arg_i` resolves to either a captured constant (broadcast) or a row-accessor (zip). + - Column-vector backends (in-memory analytics): determine `N` from the parent context; materialize each scalar as a length-`N` vector via broadcast; then run the operator element-wise. +- **Subtract / divide ordering**: same as their single-value siblings — left-to-right fold over the `values` array. For `eachSubtract([a, b, c])` over `N` rows, position `i` evaluates `a[i] - b[i] - c[i]`. +- **Aggregate over per-row arithmetic**: `sum` / `min_*` / `max_*` / `average_*` already accepted `*ArrayReturning` as `arg`; with `eachX` now in those unions, the same dispatch covers expressions like `sum(eachMultiply(field_a, field_b))` without additional cases. +- **Unit choice for date / time / datetime math**: days for `date`; seconds for `time` and `datetime`. Larger units are obtained by composition — e.g. "add N hours to a datetime" is `eachDatetimeAddSeconds(dt, eachMultiply(n, 3600))`. There is intentionally no `interval` type and no per-unit operator family; interpreters should not emulate one. Negative `right` values produce subtraction. +- **Diff operator direction**: `eachDateDiffDays` / `eachTimeDiffSeconds` / `eachDatetimeDiffSeconds` all compute `left - right`, so a positive result means `left` is later. The schema fixes this convention. +- **`eachTimeAddSeconds` overflow**: time-of-day is bounded (`00:00:00`–`23:59:59.…`). Wrap / saturate / error behaviour around the day boundary is interpreter-defined — pick one and document it. +- **Division by zero / numeric overflow / null propagation**: schema doesn't constrain. Define per-backend. +- **No `each*` arithmetic on `string`**: out of scope for this release; addable later without breakage. +- **CLAUDE.md interpreter rule**: any backend that previously rejected fields under `add` / `multiply` / etc. should keep rejecting them there — the *single-value* arithmetic family is unchanged. Fields now flow exclusively through the per-row family. ### Versioning diff --git a/CLAUDE.md b/CLAUDE.md index 2e54f47..5a89603 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,16 +87,30 @@ Two parallel sets, same convention: - Single-value: `greaterThan` / `lessThan` / `greaterThanOrEqual` / `lessThanOrEqual` over `numericReturning` / `stringReturning` / `dateReturning` / etc. Use in `having`. - Per-row: `eachGreaterThan` / `eachLessThan` / `eachGreaterThanOrEqual` / `eachLessThanOrEqual`. `left` is `*ArrayReturning` (typically a field); `right` is `*Returning` (broadcast scalar) **or** `*ArrayReturning` (element-wise other field). Use in `where` / `join.on`. -### Arithmetic and date math +### Arithmetic and date / time / datetime math Same convention: - Single-value `add` / `subtract` / `multiply` / `divide` — `values` items are `numericReturning` only. Use to combine aggregates and constants (e.g. `multiply(sum(total), 0.05)`). - Per-row `eachAdd` / `eachSubtract` / `eachMultiply` / `eachDivide` — `values` items are `numericReturning | numericArrayReturning`. Use for computed per-row columns (e.g. `eachMultiply(unit_price field, quantity field)`). - Date math: `eachDateAddDays(date, n_days) → date`, `eachDateDiffDays(date1, date2) → number`. +- Time math: `eachTimeAddSeconds(time, n_seconds) → time`, `eachTimeDiffSeconds(time1, time2) → number`. - Datetime math: `eachDatetimeAddSeconds(datetime, n_seconds) → datetime`, `eachDatetimeDiffSeconds(dt1, dt2) → number`. -Larger date/time units are expressed via composition with `eachMultiply` (e.g. `eachDatetimeAddSeconds(dt, eachMultiply(days, 86400))`). No `interval` type exists. +Unit choice: days for `date`, seconds for `time` and `datetime`. Larger units are expressed via composition with `eachMultiply` (e.g. `eachDatetimeAddSeconds(dt, eachMultiply(hours, 3600))`). No `interval` type exists. Diff operators evaluate `left - right`, so a positive result means `left` is later. `eachTimeAddSeconds` overflow / wrap behaviour around `00:00:00` is intentionally interpreter-defined. + +### Broadcast vs zip: how mixed `*Returning` / `*ArrayReturning` operands evaluate + +Every `each*` operator that admits both kinds on the same slot (`values` items in arithmetic, `left` / `right` in date/time math, `right` in comparisons, etc.) uses the same evaluation model: + +1. The surrounding row set fixes a row count `N` — determined by `from` + `joins` + `where` for `where` / `select` / `join.on` expressions, or by the group size for expressions inside an aggregate `arg` after `groupBy`. +2. Each `*Returning` (single-value) operand is **broadcast** — conceptually repeated `N` times so it has one value per row. +3. Each `*ArrayReturning` operand is already aligned with the same `N` rows by construction (it comes from the same row set). +4. The operation runs **element-wise across all operands**, producing a length-`N` result vector. + +So `eachAdd([fieldA, scalar, fieldB])` over 3 rows with `fieldA = [10, 20, 30]`, `scalar = 5`, `fieldB = [1, 2, 3]` evaluates to `[16, 27, 38]`. Mixing kinds is intended and is how common patterns are expressed: `eachMultiply(unit_price, 1.05)` adds a 5% per-row markup; `eachAdd(base_price, tax, shipping)` sums three columns per row. + +This is why `eachX` operators do not collapse to their single-value siblings when handed only scalar operands: the *return type* is still `*ArrayReturning`, aligned with the row set. `eachAdd(2, 3)` in a `select` over a 4-row entity yields the vector `[5, 5, 5, 5]`, not the scalar `5`. Use single-value `add` for purely scalar work; `eachAdd` exists specifically because at least one operand is row-aligned. ### `joinItem` has no alias diff --git a/PureQL-Specification.json b/PureQL-Specification.json index 4a32aa3..302bdcd 100644 --- a/PureQL-Specification.json +++ b/PureQL-Specification.json @@ -522,6 +522,9 @@ }, { "$ref": "#/definitions/eachDatetimeArithmetics/eachDatetimeDiffSeconds" + }, + { + "$ref": "#/definitions/eachTimeArithmetics/eachTimeDiffSeconds" } ] }, @@ -564,6 +567,9 @@ }, { "$ref": "#/definitions/arrayParameters/timeArrayParameter" + }, + { + "$ref": "#/definitions/eachTimeArithmetics/eachTimeAddSeconds" } ] }, @@ -1284,6 +1290,74 @@ } } }, + "eachTimeArithmetics": { + "eachTimeAddSeconds": { + "type": "object", + "required": [ + "operator", + "left", + "right" + ], + "properties": { + "operator": { + "const": "eachTimeAddSeconds" + }, + "left": { + "oneOf": [ + { + "$ref": "#/definitions/timeReturning" + }, + { + "$ref": "#/definitions/timeArrayReturning" + } + ] + }, + "right": { + "oneOf": [ + { + "$ref": "#/definitions/numericReturning" + }, + { + "$ref": "#/definitions/numericArrayReturning" + } + ] + } + } + }, + "eachTimeDiffSeconds": { + "type": "object", + "required": [ + "operator", + "left", + "right" + ], + "properties": { + "operator": { + "const": "eachTimeDiffSeconds" + }, + "left": { + "oneOf": [ + { + "$ref": "#/definitions/timeReturning" + }, + { + "$ref": "#/definitions/timeArrayReturning" + } + ] + }, + "right": { + "oneOf": [ + { + "$ref": "#/definitions/timeReturning" + }, + { + "$ref": "#/definitions/timeArrayReturning" + } + ] + } + } + } + }, "booleanOperations": { "or": { "type": "object", diff --git a/README.md b/README.md index e43bf5b..01436d9 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ Every operator in the schema belongs to one of two parallel families that you ch | Family | Operands | Result | Operators | |---|---|---|---| | **Single-value** | reduce to one value per query (or per group) | one value | `and`, `or`, `not`, `equal`, `greaterThan`/`lessThan`/…, `add`/`subtract`/`multiply`/`divide`, aggregates | -| **Per-row (`each*`)** | at least one operand is a field or per-row computed column | one value **per row** | `eachAnd`/`eachOr`/`eachNot`, `eachEqual`, `eachGreaterThan`/`eachLessThan`/…, `eachAdd`/`eachSubtract`/`eachMultiply`/`eachDivide`, `eachDateAddDays`/`eachDateDiffDays`, `eachDatetimeAddSeconds`/`eachDatetimeDiffSeconds` | +| **Per-row (`each*`)** | at least one operand is a field or per-row computed column | one value **per row** | `eachAnd`/`eachOr`/`eachNot`, `eachEqual`, `eachGreaterThan`/`eachLessThan`/…, `eachAdd`/`eachSubtract`/`eachMultiply`/`eachDivide`, `eachDateAddDays`/`eachDateDiffDays`, `eachTimeAddSeconds`/`eachTimeDiffSeconds`, `eachDatetimeAddSeconds`/`eachDatetimeDiffSeconds` | Where each family fits: @@ -360,6 +360,18 @@ Use in `select` for computed columns, inside aggregates (`sum(eachMultiply(unit_ } ``` +#### Broadcast and zip: mixing single-value and array operands + +Any `each*` slot that accepts both `*Returning` and `*ArrayReturning` follows the same evaluation rule. The surrounding row set fixes a row count `N` (from `from` + `joins` + `where`, or from the group size when nested inside an aggregate after `groupBy`). Then: + +- Each `*Returning` operand is **broadcast** — repeated `N` times so it has one value per row. +- Each `*ArrayReturning` operand is already aligned with `N` rows by construction (same query context). +- The operator runs **element-wise** across all operands, producing a length-`N` result vector. + +So `eachAdd([fieldA, scalar, fieldB])` over three rows with `fieldA = [10, 20, 30]`, `scalar = 5`, `fieldB = [1, 2, 3]` evaluates to `[16, 27, 38]`. This is what makes patterns like `eachMultiply(unit_price, 1.05)` (5% per-row markup) and `eachAdd(base_price, tax, shipping)` (sum three columns per row) work naturally. + +The return type is always `*ArrayReturning` even if every operand is a scalar — `eachAdd(2, 3)` in a `select` over a 4-row table yields the vector `[5, 5, 5, 5]`, not the scalar `5`. Use the single-value `add` for purely scalar work. + ### Date math (`eachDateAddDays` / `eachDateDiffDays`) Per-row date arithmetic, days as the unit. @@ -401,6 +413,26 @@ Per-row datetime arithmetic, seconds as the unit. Larger units are expressed by } ``` +### Time math (`eachTimeAddSeconds` / `eachTimeDiffSeconds`) + +Per-row time-of-day arithmetic, seconds as the unit. Same broadcast / zip rules as the rest of the `each*` family. + +| Operator | Shape | Returns | +|---|---|---| +| `eachTimeAddSeconds` | `{ left: time, right: number }` | `time` (per row) | +| `eachTimeDiffSeconds` | `{ left: time, right: time }` | `number` (per row) | + +`eachTimeAddSeconds` overflow behaviour around `00:00:00` (wrap, saturate, error) is interpreter-defined — the schema doesn't constrain it. + +```json +{ + "operator": "eachTimeAddSeconds", + "left": { "entity": "shifts", "field": "clock_in", "type": { "name": "time" } }, + "right": { "type": { "name": "number" }, "value": 1800 }, + "alias": "break_start" +} +``` + ### Aggregates Aggregates reduce an array of field values to a single value. The `arg` is any array-returning expression (typically a field reference). @@ -458,3 +490,4 @@ The [`samples/`](samples/) directory contains query examples ordered by complexi | [`18_aggregate_of_each_multiply.json`](samples/18_aggregate_of_each_multiply.json) | `sum(eachMultiply(unit_price, quantity))` grouped by user — line-item revenue | | [`19_each_date_add_days.json`](samples/19_each_date_add_days.json) | `eachDateAddDays` to derive a `delivery_eta` column from `order_date + 30 days` | | [`20_each_datetime_diff_where.json`](samples/20_each_datetime_diff_where.json) | `eachDatetimeDiffSeconds` inside `eachGreaterThan` to filter orders by ship-time | +| [`21_each_time_math.json`](samples/21_each_time_math.json) | `eachTimeAddSeconds` (time + offset) and `eachTimeDiffSeconds` (shift duration) | diff --git a/samples/21_each_time_math.json b/samples/21_each_time_math.json new file mode 100644 index 0000000..f167d9a --- /dev/null +++ b/samples/21_each_time_math.json @@ -0,0 +1,20 @@ +{ + "from": { "entity": "shifts" }, + "select": [ + { "entity": "shifts", "field": "id", "type": { "name": "uuid" } }, + { "entity": "shifts", "field": "clock_in", "type": { "name": "time" } }, + { "entity": "shifts", "field": "clock_out","type": { "name": "time" } }, + { + "operator": "eachTimeAddSeconds", + "left": { "entity": "shifts", "field": "clock_in", "type": { "name": "time" } }, + "right": { "type": { "name": "number" }, "value": 1800 }, + "alias": "break_start" + }, + { + "operator": "eachTimeDiffSeconds", + "left": { "entity": "shifts", "field": "clock_out", "type": { "name": "time" } }, + "right": { "entity": "shifts", "field": "clock_in", "type": { "name": "time" } }, + "alias": "shift_seconds" + } + ] +}