Skip to content

Feat/iso temporal values#77

Merged
matiastoro merged 3 commits into
mainfrom
feat/iso-temporal-values
Jul 9, 2026
Merged

Feat/iso temporal values#77
matiastoro merged 3 commits into
mainfrom
feat/iso-temporal-values

Conversation

@jeanpaulduchens

Copy link
Copy Markdown
Collaborator

feat(temporal): valores ISO §20.27 DATE y LOCAL DATETIME como valores de consulta

Rama: feat/iso-temporal-valuesmain
Alcance: primer avance del subsistema temporal ISO/IEC 39075:2024
(§4.16.6, §20.27), completo y probado dentro de su alcance. Query-time only.

Resumen

Agrega los tipos temporales DATE y LOCAL DATETIME como valores de
consulta, con la misma disciplina por etapas que tuvieron los paths: todo lo
que queda fuera falla con un error explícito en vez de funcionar a medias.
Ninguna consulta devuelve resultados incorrectos por usar temporales; algunas
dicen "esto aún no".

Motivación

Salió de la evaluación de conformidad: LDBC IC10 queda fuera porque su
predicado de cumpleaños necesita extraer el mes y el día de una fecha, y
GQL define constructores de valores temporales pero no extractores. Esta rama
comprueba esa lectura con código: implementa los constructores §20.27, y
confirma que aun con ellos IC10 sigue sin forma ISO-conforme (lo que falta son
los extractores, que el estándar no tiene).

Qué cambia

Modelo de valores (src/model/value.rs)

  • Value::Date(i32) = días desde 1970-01-01 (Gregoriano proléptico).
  • Value::LocalDatetime(i64) = milisegundos desde esa medianoche, sin zona
    horaria.
  • Conversión civil↔días sin dependencias (algoritmos de calendario en la línea
    clásica; el Gregoriano se repite exacto cada 400 años). Parseo ISO-8601
    estricto con validación de calendario (años bisiestos incluidos) y formato
    de vuelta.

Constructores §20.27 (src/parser/grammar.rs), soft keywords (sólo la
forma de llamada es especial; date sigue usable como variable/label):

  • DATE('2010-01-05') / LOCAL_DATETIME('2010-01-05T08:30:15') desde string.
  • DATE({year:, month:, day:}) desde record de campos (sintaxis del estándar).
  • DATE() / LOCAL_DATETIME() sin argumento = los equivalentes de
    CURRENT_DATE / LOCAL_TIMESTAMP (SR 1-3).
  • String o record malformado degrada a Null (convención failure-as-null de
    CAST).

Typechecker (src/typing/)

  • SimpleType::Date / LocalDatetime como tipos base: meet, refine y
    unión funcionan; ordenables per §22.14 sin requerir GA04, por lo que son
    llaves válidas de ORDER BY, GROUP BY y DISTINCT.
  • Constructor tipado por su resultado (DATE(x) : DATE); un argumento
    probadamente incorrecto (DATE(true)) es error duro de compilación.
    Distinción tipo-vs-valor: DATE(true) falla por tipo (rechazo estático),
    DATE('hola') falla por valor (→ Null en runtime, como CAST).

Runtime (src/runtime/)

  • Orden total dentro de cada tipo (WHERE, ORDER BY, MIN/MAX); igualdad
    y hashing para GROUP BY y DISTINCT; tipos distintos nunca comparan.
  • Aritmética §20.26 MVP: LOCAL_DATETIME(...) ± <enteros de milisegundos>, la
    forma que produce el desazucarado existente de DURATION({...}).
  • Fix de despacho: el despacho genérico de operadores binarios coercionaba
    ambos operandos a numérico antes de evaluar, lo que degradaba silenciosamente
    los temporales a Null. Se agrega un camino tipado para pares temporales y
    se deja la coerción intacta para el resto (de paso se elimina una doble
    evaluación de operandos en esa ruta).

Bindings y utilidades

  • Python / Node / WASM devuelven strings ISO-8601.
  • REPL y dumps imprimen sintaxis de constructor (DATE('...')), round-trippable.

Fuera de alcance (documentado, con error explícito)

  • Tipos ZONED_* (zonas horarias reales: IANA + horario de verano).
  • DURATION como tipo propio (su álgebra separa año-mes de día-tiempo, que no
    conmutan).
  • Aritmética completa de §20.26 / §20.28 (fecha ± duración, resta de datetimes,
    DURATION_BETWEEN).
  • Almacenamiento de temporales como propiedad: value_to_prop lo rechaza con
    error explícito, el mismo trato que Path.

Tests

tests/temporal_test.rs (13 casos): las tres formas de construcción, orden
cronológico en ORDER BY, agrupación por fecha, no comparación entre tipos
distintos, validación de calendario (DATE('2023-02-29')Null; el bisiesto
2024-02-29 sí válido), aritmética con duraciones, y los dos comportamientos
del typechecker (aceptar fechas como llave de orden, rechazar DATE(true)).

Diámetro del cambio

17 archivos, +655 líneas. Rama desde origin/main (v0.2.7); mergea limpio.

Pre-commit

cargo fmt --all · cargo clippy --workspace --all-targets -- -D clippy::all
· sweep completo de tests + suites sensibles a expresiones
(floor/cast, división, records, ic7, aggregates): verde, cero regresiones.

Notas para el revisor

  • La representación como enteros (días / millis) es la decisión central: hace
    que comparar, ordenar y agrupar fechas reutilice toda la maquinaria existente
    sin cambios.
  • Los tipos temporales son tipos base, así que encajan sin fricción con la
    decisión ★ = base-only del chequeo de ORDER BY.

Jean Paul Duchens Pacheco and others added 2 commits July 5, 2026 03:24
MVP of the ISO temporal subsystem, staged like Path values were:

- Value::Date (days since epoch, proleptic Gregorian) and
  Value::LocalDatetime (millis, no timezone), with dependency-free
  civil<->days conversion, strict ISO-8601 parsing (leap years
  validated) and round-trippable formatting.
- §20.27 constructors as soft keywords (call form only): DATE()/
  LOCAL_DATETIME() = the CURRENT_* equivalents per SR 1-3, one-arg
  forms take a <date/datetime string> or a field record. Malformed
  input degrades to Null, the CAST failure-as-null convention.
- Typechecker: SimpleType::Date / LocalDatetime are base types,
  orderable per §22.14 without GA04; provably wrong constructor
  arguments are hard errors.
- Runtime: total order within each type (WHERE / ORDER BY / MIN/MAX),
  equality + hashing for GROUP BY and DISTINCT, cross-type pairs never
  compare, and §20.26 MVP arithmetic (LOCAL_DATETIME ± integer millis,
  the form the DURATION({...}) desugaring produces).
- The generic Binop dispatch now routes temporal operand pairs past
  the numeric-cast coercion (which silently nulled them) and inlines
  the delta cast checks without re-evaluating operands.
- Out of scope, documented: ZONED_* (timezones), typed durations, and
  temporal property storage (value_to_prop rejects them explicitly,
  same treatment as Path).

Python/Node/WASM bindings return ISO-8601 strings; REPL and dumps
print constructor syntax. 13 integration tests in temporal_test.rs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jeanpaulduchens jeanpaulduchens self-assigned this Jul 6, 2026
@jeanpaulduchens jeanpaulduchens added the enhancement New feature or request label Jul 6, 2026
…rción de operandos

El merge de la 3VL (#73) rompía pushdown_null_test: el despacho de la
rama temporal inlineó el cast de operandos pero omitió el caso 'null
inhabits every type' que la 3VL agregó al brazo AS. Un operando null
devolvía Failure en vez de fluir como Null a la lógica 3VL, de modo que
'x.a = 999 OR true' con x.a ausente tiraba la fila en vez de conservarla.
Ahora la coerción inline trata null explícitamente, idéntico al brazo AS
de main; los temporales conservan su bypass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@matiastoro matiastoro merged commit 8226189 into main Jul 9, 2026
4 checks passed
matiastoro added a commit that referenced this pull request Jul 9, 2026
Bindings open() now creates the DB when the path is absent (issue raised
via JS/VS Code usage). Python and Node open() switched from
LazyGraphStore::open to open_or_create, matching the CLI REPL and
sqlite3.connect: a missing path yields an empty DB (DEFAULT active) ready
for INSERT + save(). Existing-path behaviour unchanged.

Migrate node/ to @napi-rs/cli 3.x (root fix for the VS Code extension-host
crash). The 2.x-generated loader read process.report.getReport().header
unguarded; getReport() returns undefined in the Linux extension host, so
.header threw 'Cannot read properties of undefined' and aborted activation
before any frogql call. napi-cli 2.x (2.18.4, the last 2.x) still emits the
unguarded form, so the fix is the 3.x toolchain whose loader detects musl
through a guarded filesystem -> process.report -> child-process chain,
falling back to glibc when the report is unavailable (frogql ships only
linux-*-gnu).

Migration details:
- crates napi 3.10 / napi-derive 3.5 (napi-build stays 2 — no 3.x). Pin
  >=3.10: napi-derive 3.5's generated code references bindgen_prelude
  symbols absent from napi 3.8. Binding source (607 lines) unchanged.
- package.json config napi.binaryName + napi.targets (2.x name + triples
  deprecated); build scripts drop the stopgap loader patch.
- release-npm.yml: create-npm-dir -> create-npm-dirs, artifacts gets
  explicit --output-dir/--npm-dir, aarch64-linux --zig -> --use-napi-cross
  (zig setup step removed).

Verified: cargo fmt + clippy clean, full cargo test sweep green, node smoke
8/8, typecheck OK, loader loads with getReport()->undefined, open_or_create
create-on-open confirmed end-to-end. Bundles the already-merged ISO temporal
values (PR #77) into the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
matiastoro added a commit that referenced this pull request Jul 9, 2026
… 1.97)

Pre-existing lint inherited from the ISO temporal PR (#77), which merged
with a red CI. clippy 1.97's question_mark lint flags the
`match Option { Some(v) => v.len(), None => return None }` in the
btree-ltj-real top-k path; `?` is semantically identical. Local clippy was
1.95 (didn't flag it); bumped the toolchain to 1.97 to match CI. No
behaviour change — the already-published v0.2.8 artifact compiles fine
(clippy is a lint, not a compile error); this just greens main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants