Skip to content

Commit cc55732

Browse files
Merge pull request #108 from VH-Lab/claude/did-hasmember-quoting
hasmember: one rule in field_search, as in DID-matlab (#236)
2 parents d7cf938 + 780a3f7 commit cc55732

4 files changed

Lines changed: 362 additions & 6 deletions

File tree

‎src/did/datastructures.py‎

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import numbers
23
import re
34

45
import numpy as np
@@ -190,6 +191,37 @@ def or_operands(search_struct):
190191
return operands
191192

192193

194+
def _has_member(value, member):
195+
"""Is *member* a member of *value*, for field_search's 'hasmember'?
196+
197+
Text is a member of text that is the whole value or an item of its
198+
comma-separated list (white space after a comma, and before one, is
199+
ignored); anything is a member of a list that holds an equal element; a
200+
number is a member of an equal number. Text is never a member of a
201+
number, nor a number of text. This is what DID-matlab's fieldsearch and
202+
its SQL search match (DID-matlab#236).
203+
"""
204+
205+
def is_number(x):
206+
return isinstance(x, numbers.Real) and not isinstance(x, str)
207+
208+
if isinstance(value, np.ndarray):
209+
value = value.tolist()
210+
if isinstance(member, str):
211+
if isinstance(value, str):
212+
pattern = r"(^|,\s*)" + re.escape(member) + r"\s*(,|$)"
213+
return re.search(pattern, value) is not None
214+
if isinstance(value, (list, tuple)):
215+
return any(isinstance(v, str) and v == member for v in value)
216+
return False
217+
if is_number(member):
218+
if is_number(value):
219+
return value == member
220+
if isinstance(value, (list, tuple)):
221+
return any(is_number(v) and v == member for v in value)
222+
return False
223+
224+
193225
def field_search(a, search_struct):
194226
"""
195227
Searches a dictionary to determine if it matches a search structure.
@@ -260,10 +292,7 @@ def field_search(a, search_struct):
260292
b = eq_len(np.array(value).shape, param1)
261293
elif op_lower == "hasmember":
262294
if is_there:
263-
try:
264-
b = param1 in value
265-
except TypeError:
266-
pass
295+
b = _has_member(value, param1)
267296
elif op_lower == "hasfield":
268297
b = is_there
269298
elif op_lower == "partial_struct":

‎src/did/did_matlab_python_bridge.yaml‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ classes:
4040
- name: database
4141
type: class
4242
matlab_path: "+did/database.m"
43-
matlab_last_sync_hash: "2c9d8fa7"
43+
matlab_last_sync_hash: "74ae15f1"
4444
status: ported
4545
python_path: "did/database.py"
4646
python_class: "Database"
@@ -124,6 +124,25 @@ classes:
124124
did/database.py nor did/implementations/sqlitedb.py changes.
125125
tests/test_negated_or.py pins the behaviour with the same cases as
126126
MATLAB's did.unittest.TestNegatedOr.
127+
128+
DID-matlab c8adf741..74ae15f1 (DID-matlab#236, 2026-10-24):
129+
query_struct_to_sql_str put a text 'hasmember' value into the SQL
130+
unquoted (and as a double-quoted token, which SQLite reads as a column
131+
when it names one), so 'value' matched every document and '1 OR 1=1'
132+
was SQL injection; and the SQL could not see the members of an array
133+
or cell, which doc2sql stores only as '[1 3] double'. MATLAB now
134+
searches hasmember by brute force with fieldsearch, as Python always
135+
has: the new protected static isBruteForceTerm (hasmember, and the
136+
depends_on queries of isBruteForceDependsOn) sends a term to
137+
brute_force_search, searched last in an AND and kept out of the OR
138+
batches, and query_struct_to_sql_str's 'hasmember' case is an error.
139+
That is what SQLiteDB already does (_query_struct_to_sql_str returns
140+
None for hasmember; _SCAN_OPERATIONS ranks it last), so did/database.py
141+
and did/implementations/sqlitedb.py do not change; the rule itself
142+
changed in fieldsearch and field_search -- see the fieldsearch entry.
143+
2c08472c merges DID-matlab main (2c9d8fa7, #237, above) into that
144+
branch. tests/test_hasmember_quoting.py mirrors DID-matlab's
145+
TestHasMemberQuoting.
127146
sync_notes: >
128147
Schema validation was ported on 2026-08-28, closing what had been the
129148
largest capability gap between the two languages. Python previously had

‎src/did/did_matlab_python_bridge_util.yaml‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ functions:
317317
- name: fieldsearch
318318
type: function
319319
matlab_path: "+did/+datastructures/fieldsearch.m"
320-
matlab_last_sync_hash: "051f535"
320+
matlab_last_sync_hash: "f4ed409e"
321321
status: ported
322322
python_path: "did/datastructures.py"
323323
python_name: "field_search"
@@ -330,6 +330,20 @@ functions:
330330
holds a non-struct value (depends_on = [] read back from a database)
331331
used to return early, skipping the negation; it now matches. Python's
332332
field_search already negated it; nothing to port.
333+
DID-matlab f4ed409e (DID-matlab#236, 2026-10-24): 'hasmember' had a
334+
different rule in each place. MATLAB's fieldsearch used
335+
ismember(param1, value), which compares two texts character by
336+
character; its SQL search matched an item of a comma-separated list;
337+
field_search used `in` (a substring test, and a TypeError -- no match
338+
-- on a scalar number). All three now match one rule, MATLAB's in the
339+
local function fieldsearch/hasMember and Python's in _has_member: text
340+
is a member of text that is the whole value or an item of its
341+
comma-separated list (pattern (^|,\s*)M\s*(,|$)); anything is a
342+
member of a list (MATLAB cell or numeric array) that holds an equal
343+
element, and a number of an equal number; text is never a member of a
344+
number, nor a number of text; a dict (MATLAB struct) has no members.
345+
tests/test_hasmember_quoting.py test_rule runs the same table of
346+
cases as DID-matlab's TestHasMemberQuoting/testRule.
333347
334348
- name: orOperands
335349
type: function

0 commit comments

Comments
 (0)