Skip to content

fix: preserve SQLITE_NULL type for NULL values in BOOLEAN / DATETIME columns - #883

Open
antoineleclair wants to merge 1 commit into
canonical:mainfrom
antoineleclair:main
Open

fix: preserve SQLITE_NULL type for NULL values in BOOLEAN / DATETIME columns#883
antoineleclair wants to merge 1 commit into
canonical:mainfrom
antoineleclair:main

Conversation

@antoineleclair

Copy link
Copy Markdown

Fixes #882.

value_type() in src/query.c was remapping NULL values to
DQLITE_BOOLEAN / DQLITE_ISO8601 based on the column's declared type,
so a NULL in a BOOLEAN column was sent on the wire as BOOLEAN(11)
with value 0 (indistinguishable from FALSE) and a NULL in a DATETIME
/ DATE / TIMESTAMP column was sent as ISO8601(10) with "". The
fix returns early when sqlite3_column_type() reports SQLITE_NULL,
matching plain SQLite's behaviour.

Adds a regression test
(test/integration/test_client.c::client/nullPreservedAcrossDeclaredTypes)
that asserts every column of a row of NULLs comes back with
type == SQLITE_NULL, covering BOOLEAN, DATETIME, DATE, TIMESTAMP
as well as the unaffected types as a sanity check.

Running the same code samples that reproduced the issue in #882
shows that all types appear correctly as NULL.

C (null_types_repro.c):

    === SQLite (sqlite3_column_type for each column) ===
      int_col       : NULL                              ok
      text_col      : NULL                              ok
      real_col      : NULL                              ok
      blob_col      : NULL                              ok
      bool_col      : NULL                              ok
      datetime_col  : NULL                              ok
      date_col      : NULL                              ok
      timestamp_col : NULL                              ok

    === dqlite (wire-level type tag from server) ===
      int_col       : NULL                              ok
      text_col      : NULL                              ok
      real_col      : NULL                              ok
      blob_col      : NULL                              ok
      bool_col      : NULL                              ok
      datetime_col  : NULL                              ok
      date_col      : NULL                              ok
      timestamp_col : NULL                              ok

Go (main.go, via go-dqlite):

    === SQLite ===
      int_col       : NULL                       ok
      text_col      : NULL                       ok
      real_col      : NULL                       ok
      blob_col      : NULL                       ok
      bool_col      : NULL                       ok
      datetime_col  : NULL                       ok
      date_col      : NULL                       ok
      timestamp_col : NULL                       ok

    === dqlite ===
      int_col       : NULL                       ok
      text_col      : NULL                       ok
      real_col      : NULL                       ok
      blob_col      : NULL                       ok
      bool_col      : NULL                       ok
      datetime_col  : NULL                       ok
      date_col      : NULL                       ok
      timestamp_col : NULL                       ok

Python (null_types_repro.py, via dqlite-client / dqlite-wire):

    === SQLite (stdlib sqlite3) ===
      int_col       : NULL                              ok
      text_col      : NULL                              ok
      real_col      : NULL                              ok
      blob_col      : NULL                              ok
      bool_col      : NULL                              ok
      datetime_col  : NULL                              ok
      date_col      : NULL                              ok
      timestamp_col : NULL                              ok

    === dqlite (wire-level type tag from server) ===
      int_col       : NULL                              ok
      text_col      : NULL                              ok
      real_col      : NULL                              ok
      blob_col      : NULL                              ok
      bool_col      : NULL                              ok
      datetime_col  : NULL                              ok
      date_col      : NULL                              ok
      timestamp_col : NULL                              ok

When encoding query results, value_type() remaps certain column types
(BOOLEAN, DATETIME) to dqlite-specific type codes. However, it was also
doing this for NULL values, causing them to be encoded as their "zero"
values (0 for BOOLEAN, "" for DATETIME) instead of NULL.

This makes it impossible for clients to distinguish:
- BOOLEAN NULL vs FALSE
- DATETIME NULL vs empty string

SQLite's sqlite3_column_type() correctly returns SQLITE_NULL for NULL
values regardless of declared column type. This change preserves that
behavior by returning early when the value is NULL.

Example demonstrating SQLite's behavior:

  CREATE TABLE t (b BOOLEAN, d DATETIME);
  INSERT INTO t VALUES (NULL, NULL);
  INSERT INTO t VALUES (0, '');

  -- sqlite3_column_type() returns:
  -- Row 1: NULL (5), NULL (5)     <- both are SQLITE_NULL
  -- Row 2: INTEGER (1), TEXT (3)  <- actual types

Before this fix, dqlite would return:
  -- Row 1: BOOLEAN (11) with 0, ISO8601 (10) with ""
  -- Row 2: BOOLEAN (11) with 0, ISO8601 (10) with ""

After this fix:
  -- Row 1: NULL (5), NULL (5)
  -- Row 2: BOOLEAN (11) with 0, ISO8601 (10) with ""
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.

NULL values in BOOLEAN / DATETIME / DATE / TIMESTAMP columns are sent on the wire as zero-valued non-NULL types

1 participant