Skip to content

hasmember: one rule, searched by brute force as in DID-python (#236) - #239

Merged
stevevanhooser merged 6 commits into
mainfrom
claude/did-hasmember-quoting
Oct 24, 2026
Merged

stevevanhooser merged 6 commits into
mainfrom
claude/did-hasmember-quoting

Conversation

@stevevanhooser

@stevevanhooser stevevanhooser commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Closes #236.

This MATLAB code has not been run locally. There is no MATLAB in the environment it was written in. The tests run in this PR's CI.

Problem

  1. SQL injection / wrong results. query_struct_to_sql_str put a text hasmember value into the SQL unquoted:
    • 'name17' → no such column
    • 'value' → the column doc_data.value, so every document matched
    • '1 OR 1=1' → injection, every document matched
    • a value containing " or ' → syntax error
  2. SQL could not see list members. doc2sql stores an array or cell with more than one element only as a summary, e.g. '[1 3] double'. So in SQL, hasmember 2 missed [1 2 3], and ~hasmember 2 matched it.
  3. Three different rules:
    • fieldsearch used ismember, which compares two texts character by character. It returns a logical array, and ~/AND then treat that array element by element.
    • the SQL search matched a comma-list item.
    • DID-python's field_search tested for a substring.

Fix

One rule (src/did/+did/+datastructures/fieldsearch.m)

hasmember now calls a new local function, hasMember:

  • text in text: the member is the whole value or an item of its comma-separated list ((^|,\s*)M\s*(,|$), with M escaped)
  • anything in a cell array: an element equals the member
  • a number in a numeric or logical array: an element equals it
  • text is never a member of a number, nor a number of text. A struct has no members.

DID-python's field_search gets the same rule (VH-Lab/DID-python#108).

Brute force, as DID-python (src/did/+did/database.m)

  • A new protected static method, isBruteForceTerm, is true for every hasmember/~hasmember, and for the depends_on queries isBruteForceDependsOn already sent to brute force.
  • search_doc_ids sends those terms to brute_force_search, which tests documents one by one with fieldsearch. They are still searched last in an AND, against the candidates the other terms leave.
  • search_or_operands keeps them out of its batches.
  • query_struct_to_sql_str's hasmember case is now an error, since no search reaches it. No hasmember value goes into SQL.

This is what DID-python's SQLiteDB already does: _query_struct_to_sql_str returns None for hasmember. The two languages now apply the same rule in the same way. Cost: a hasmember that isn't narrowed by an AND (to at most maxSearchCandidates) loads every document of the branch, as in Python.

main (#238, the #237 ~or fix) is merged in, with no conflict.

Tests

TestHasMemberQuoting (new)

  • testRule: fieldsearch against a table of 19 value/member cases, each also negated. The same table is in DID-python's test_rule.
  • db.search compared with fieldsearch for:
    • text values ('value', '1 OR 1=1', 'say "hi"', 'O''Brien', 'x, value', 'name17', 'zzz', '5', '')
    • numbers, including Inf, -Inf and NaN
    • each also as ~hasmember
  • text and numbers don't mix (5 in the text '5').
  • a struct field has no members.
  • a field no document has: ~hasmember matches every document.
  • array and cell fields ([1 2 3], {'alpha','beta'}), which SQL could not search.
  • inside an AND: only the 3 candidates are loaded, and the branch is not listed.
  • inside an OR: the results match, and no SQL search is run.

TestBatchedOr

hasmember moves out of the SQL-term list (allTerms) into a new testBruteForceHasMemberInAnOr, next to the brute-forced depends_on test.

Related

🤖 Generated with Claude Code

https://claude.ai/code/session_01C7XsYS2DKSy2ucFzEEWR3D

query_struct_to_sql_str put a hasmember value into the SQL unquoted, and
also compared it as a double-quoted token. A text value was then read as
SQL: 'name17' as a column (no such column), 'value' as doc_data.value
(every document matched) and '1 OR 1=1' as an expression (SQL
injection). The double-quoted test had the same problem for "value".

The list-member regex and the whole-value test now use single-quoted
literals (sqlStringList). doc2sql stores a number as a number, which never
equals a text literal, so a finite numeric value is also compared as a
number, written by sprintf('%.17g') so it can only be a numeric literal.

TestHasMemberQuoting compares db.search with fieldsearch for text values
(including 'value', '1 OR 1=1' and values holding " and '), numbers and
~hasmember, alone, inside an AND (restricted to candidates) and inside an
OR (searched in a batch).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7XsYS2DKSy2ucFzEEWR3D
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

545 tests  +30   545 ✅ +30   2m 44s ⏱️ -8s
 44 suites + 1     0 💤 ± 0 
  1 files   ± 0     0 ❌ ± 0 

Results for commit 74ae15f. ± Comparison against base commit 6f9be24.

♻️ This comment has been updated with latest results.

CI showed that fieldsearch is no oracle for a text hasmember:
ismember(param1, value) compares two char arrays character by character,
and the logical array it returns is negated and ANDed element by element,
so '~hasmember value' matched only names sharing no character with 'value'
and an AND matched any name sharing one. The SQL results in those runs were
the intended ones.

The expected documents now come from matches/memberOf: hasmember holds when
the value is the whole field or an item of its comma-separated list, and
every other operation is still tested by fieldsearch. Number searches are
also compared with fieldsearch directly, which agrees for them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7XsYS2DKSy2ucFzEEWR3D
@codecov

codecov Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.15385% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 67.53%. Comparing base (6f9be24) to head (74ae15f).

Files with missing lines Patch % Lines
src/did/+did/database.m 87.50% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #239      +/-   ##
==========================================
+ Coverage   67.40%   67.53%   +0.13%     
==========================================
  Files          67       67              
  Lines        4807     4821      +14     
==========================================
+ Hits         3240     3256      +16     
+ Misses       1567     1565       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

fieldsearch tested hasmember with ismember(param1, value), which compares
two texts character by character and returns a logical array, so it
matched another rule than the SQL search did (and DID-python's
field_search, a substring test, a third). All three now match one rule,
in a new local function fieldsearch/hasMember:

- text is a member of text that is the whole value or an item of its
  comma-separated list (white space around the comma ignored);
- anything is a member of a cell array that holds an equal element, and
  a number of a numeric or logical array that holds it;
- text is never a member of a number, nor a number of text.

query_struct_to_sql_str matches the same rule: a text member is tested
only against text values (typeof), with the pattern (^|,\s*)M\s*(,|$),
and a numeric member only against numbers. It no longer extends the
field to its subfields (fields.field_name like 'field.%'), so a struct
has no members, as in fieldsearch.

TestHasMemberQuoting now compares db.search with fieldsearch again, and
testRule pins fieldsearch to a table of cases shared with DID-python.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7XsYS2DKSy2ucFzEEWR3D
@stevevanhooser stevevanhooser changed the title SQLite search: quote the text tests of 'hasmember' (#236) hasmember: one rule in fieldsearch and SQL; never read as SQL (#236) Oct 24, 2026
hasmember Inf / -Inf (written as 9e999 in the SQL) and NaN (a member of
nothing) match no document, and ~hasmember matches every one; this
covers the two SQL branches the other cases did not reach.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7XsYS2DKSy2ucFzEEWR3D
doc2sql stores an array or cell of more than one element only as a summary
of its size and class ('[1 3] double'), so the SQL search could not see the
members of such fields: hasmember missed them and ~hasmember matched them.
search_doc_ids now tests every hasmember and ~hasmember term with
fieldsearch (brute_force_search), as DID-python's SQLiteDB does, so both
languages apply the same rule the same way and no hasmember value goes into
SQL. The new isBruteForceTerm names the terms searched this way (hasmember,
and the depends_on queries isBruteForceDependsOn already sent there);
search_doc_ids still searches them last in an AND, against the candidates
the other terms leave, and search_or_operands keeps them out of its
batches. query_struct_to_sql_str's 'hasmember' case is now an error, as no
search reaches it.

Tests: TestHasMemberQuoting checks that an AND loads only its candidates,
that an OR of hasmembers runs no SQL search, and covers array and cell
fields and a missing field; TestBatchedOr moves hasmember from its SQL
terms to a brute-force-in-an-OR test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7XsYS2DKSy2ucFzEEWR3D
@stevevanhooser stevevanhooser changed the title hasmember: one rule in fieldsearch and SQL; never read as SQL (#236) hasmember: one rule, searched by brute force as in DID-python (#236) Oct 24, 2026
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.

SQLite search: 'hasmember' puts a text value into the SQL unquoted (errors, wrong results, SQL injection)

3 participants