Skip to content

feat(orm): column-typed result sets via cwist_orm_query_typed() (issue #354) - #389

Merged
gg582 merged 1 commit into
devfrom
feat/orm-typed-results
Oct 17, 2026
Merged

gg582 merged 1 commit into
devfrom
feat/orm-typed-results

Conversation

@gg582

@gg582 gg582 commented Oct 16, 2026

Copy link
Copy Markdown
Member

Summary

Issue #354's exact-64-bit ask landed earlier (single-scalar _i64 helpers); this delivers the remaining ask — result sets typed by column storage class, not text:

  • New additive API: cwist_orm_query_typed() → cwist_orm_result_t of cwist_orm_value_t cells (NULL / INTEGER / REAL / TEXT / BLOB with i64 / real / text / blob+blob_len payloads), plus cwist_orm_result_free(), cwist_orm_result_column_index(), cwist_orm_result_cell().
  • Typing happens at the producer (sqlite3_column_type() in the orm_socket worker, which moves from sqlite3_exec to a prepare/step loop and emits a parallel "typed" block; BLOBs travel base64 + length). JSON text alone carries no column-type information the consumer could recover. One documented gotcha handled: sqlite3_column_text() mutates what sqlite3_column_type() reports, so the typed cell is read first.
  • No behavior change to existing paths: legacy cwist_orm_query()'s rows payload is byte-for-byte the same (raw-socket dump + all pre-existing tests pass unmodified).

What this fixes

  • TEXT columns holding digits no longer come back as numbers ('007' stays text)
  • SQL NULL is a distinct type from empty TEXT
  • INTEGER above 2^53 is exact through the typed path (digit strings, parsed client-side — no JSON double)
  • BLOBs get a real typed path for the first time

Test plan

  • test_query_typed() in tests/test_orm.c: per-type round-trips, >2^53 exactness, NULL vs '', non-UTF-8 and empty BLOBs, mixed-type row, zero-row metadata, error paths, legacy-path contrast — make test_orm / test_orm_socket pass
  • make api-check OK, check_test_wiring.py OK, cwist audit --gate OK
  • ORM examples (micro 06/07/08/17) still build

Note for reviewers

scripts/ci/malloc-baseline.txt regeneration also absorbs the two raw-allocator lines from 37a78010 (fix(gc)) that were missing there — the malloc gate was red at dev HEAD until this lands.

…#354)

The socket-backed ORM produced result rows as text and let the client
re-type values with strtoll/strtod heuristics: numeric-looking TEXT came
back as numbers, SQL NULL was indistinguishable from empty text, INTEGER
above 2^53 lost digits through the JSON double, and BLOBs had no typed
path at all (the exact-i64 commit added single-scalar workarounds only).

Add an additive typed path alongside the unchanged legacy helpers:
  - cwist_orm_query_typed() returns a cwist_orm_result_t of
    cwist_orm_value_t cells typed by each value's SQLite storage class
    (sqlite3_column_type at the producer — JSON text alone carries no
    column-type information the consumer could recover): INTEGER as
    exact int64 through the full range, REAL as double, TEXT as string,
    BLOB as (bytes, length), NULL a distinct type from empty TEXT.
  - The worker moves from sqlite3_exec() to a prepare/step statement
    loop; the response gains a parallel 'typed' block (columns + row
    arrays of {t,v,n} cells; BLOB base64 + length). Multi-statement
    requests still merge rows as before. The typed cell is read before
    the legacy text conversion because sqlite3_column_text() mutates
    what sqlite3_column_type() reports.
  - cwist_orm_result_free(), cwist_orm_result_column_index(),
    cwist_orm_result_cell() accessors.

The legacy 'rows' payload is byte-for-byte unchanged (raw-socket dump +
unmodified pre-existing tests); all ORM examples still build.

tests/test_orm.c gains test_query_typed(): per-type round-trips, INTEGER
> 2^53 exactness through the typed path, NULL vs '', non-UTF-8 and
empty BLOBs, mixed-type row, zero-row column metadata, error paths, and
a legacy-path contrast assert. make test_orm / test_orm_socket pass,
make api-check OK, check_test_wiring OK, cwist audit --gate OK.

Regenerates scripts/ci/malloc-baseline.txt: the two raw-allocator lines
from 37a7801 (fix(gc)) that were missing there (the gate was red at
HEAD), plus this change's own raw frees/allocations consistent with
orm.c/orm_socket.c's existing raw-allocator style. Also regenerates the
v4.0 API baseline for the new public surface.
@gg582
gg582 merged commit ec85f6c into dev Oct 17, 2026
26 checks passed
gg582 added a commit that referenced this pull request Oct 17, 2026
…#354) (#389)

The socket-backed ORM produced result rows as text and let the client
re-type values with strtoll/strtod heuristics: numeric-looking TEXT came
back as numbers, SQL NULL was indistinguishable from empty text, INTEGER
above 2^53 lost digits through the JSON double, and BLOBs had no typed
path at all (the exact-i64 commit added single-scalar workarounds only).

Add an additive typed path alongside the unchanged legacy helpers:
  - cwist_orm_query_typed() returns a cwist_orm_result_t of
    cwist_orm_value_t cells typed by each value's SQLite storage class
    (sqlite3_column_type at the producer — JSON text alone carries no
    column-type information the consumer could recover): INTEGER as
    exact int64 through the full range, REAL as double, TEXT as string,
    BLOB as (bytes, length), NULL a distinct type from empty TEXT.
  - The worker moves from sqlite3_exec() to a prepare/step statement
    loop; the response gains a parallel 'typed' block (columns + row
    arrays of {t,v,n} cells; BLOB base64 + length). Multi-statement
    requests still merge rows as before. The typed cell is read before
    the legacy text conversion because sqlite3_column_text() mutates
    what sqlite3_column_type() reports.
  - cwist_orm_result_free(), cwist_orm_result_column_index(),
    cwist_orm_result_cell() accessors.

The legacy 'rows' payload is byte-for-byte unchanged (raw-socket dump +
unmodified pre-existing tests); all ORM examples still build.

tests/test_orm.c gains test_query_typed(): per-type round-trips, INTEGER
> 2^53 exactness through the typed path, NULL vs '', non-UTF-8 and
empty BLOBs, mixed-type row, zero-row column metadata, error paths, and
a legacy-path contrast assert. make test_orm / test_orm_socket pass,
make api-check OK, check_test_wiring OK, cwist audit --gate OK.

Regenerates scripts/ci/malloc-baseline.txt: the two raw-allocator lines
from 37a7801 (fix(gc)) that were missing there (the gate was red at
HEAD), plus this change's own raw frees/allocations consistent with
orm.c/orm_socket.c's existing raw-allocator style. Also regenerates the
v4.0 API baseline for the new public surface.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant