From b81efc88fa16fed7d2819caba1f5fb6e4c324165 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 02:02:19 +0000 Subject: [PATCH] docs(protocol): $exists means HAS A VALUE, not key presence (#13539) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two protocol pages still described `$exists` as a key-presence test — "Field exists (NoSQL)" and "Field existence check". That stopped being true when the has-value alignment landed at 9dac1ae017 (PR #13529): every backend now answers "the field has a value" (`!= null`), and driver-mongodb no longer emits MongoDB's own `$exists` at all. Established from code, not from another document: - packages/drivers/driver-mongodb/src/mongodb-filter.ts `case '$exists'` puts `$ne: null` / `$eq: null` — the spelling `$null` already emits; - packages/drivers/driver-sql/src/sql-driver.ts `case '$exists'` compiles `whereNotNull` / `whereNull`; - packages/objectql/src/having-filter.ts evaluates `value !== undefined && value !== null`; - packages/spec/src/data/filter-logic-conformance.ts states the settled semantic verbatim: "`$exists` means \"has a value\" (`!= null`), never key-presence". content/docs/data-modeling/queries.mdx already said "Field has a value" and is deliberately untouched — it is the target wording, not another site to sweep. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC --- content/docs/protocol/kernel/http-protocol.mdx | 2 +- content/docs/protocol/objectql/query-syntax.mdx | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/content/docs/protocol/kernel/http-protocol.mdx b/content/docs/protocol/kernel/http-protocol.mdx index b4880e72c2..925c1fbf48 100644 --- a/content/docs/protocol/kernel/http-protocol.mdx +++ b/content/docs/protocol/kernel/http-protocol.mdx @@ -394,7 +394,7 @@ GET /api/data/account?filter={"status":"active"} - `$endsWith` - String ends with - `$between` - Between two values (tuple) - `$null` - Null check (`{ "$null": true }` for IS NULL, `{ "$null": false }` for IS NOT NULL) -- `$exists` - Field existence check +- `$exists` - Field has a value — the inverse of `$null` (`{ "$exists": true }` is `IS NOT NULL`, `{ "$exists": false }` is `IS NULL`). Not a key-presence test: a stored `null` counts as no value on every backend, MongoDB included **OR conditions:** ```json diff --git a/content/docs/protocol/objectql/query-syntax.mdx b/content/docs/protocol/objectql/query-syntax.mdx index fb586b1d77..d627e567f7 100644 --- a/content/docs/protocol/objectql/query-syntax.mdx +++ b/content/docs/protocol/objectql/query-syntax.mdx @@ -261,7 +261,7 @@ const query: QueryAST = { | `$ilike` | Same pattern language, **ignoring ASCII case** | `{ name: { $ilike: '%industries' } }` | | `$between` | Range (inclusive) | `{ close_date: { $between: ['2024-01-01', '2024-12-31'] } }` | | `$null` | Null check | `{ manager_id: { $null: true } }` / `{ phone: { $null: false } }` | -| `$exists` | Field exists (NoSQL) | `{ metadata: { $exists: true } }` | +| `$exists` | Field has a value — the inverse of `$null` | `{ metadata: { $exists: true } }` | ### `$like` is not a spelling of `$contains` @@ -514,10 +514,21 @@ where: { manager_id: { $null: true } } // Field IS NOT NULL where: { phone: { $null: false } } -// Field exists (NoSQL) +// Field has a value — the same rows as { $null: false } where: { metadata: { $exists: true } } ``` + +**`$exists` is not a key-presence test.** It asks whether the field **has a +value**, so a stored `null` counts as *no value* on every backend: +`{ $exists: true }` compiles to `IS NOT NULL` and `{ $exists: false }` to +`IS NULL`. It is the exact inverse of `$null` — `{ $exists: b }` selects the +same rows as `{ $null: !b }` — and it is portable, not a NoSQL-only operator. +On MongoDB the driver lowers it to `{ $ne: null }` / `{ $eq: null }` and never +emits MongoDB's own `$exists`, which *would* have been key presence and would +have kept a `null`-valued field. + + ### Filtering Across Relationships