Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/did/did_matlab_python_bridge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ classes:
- name: database
type: class
matlab_path: "+did/database.m"
matlab_last_sync_hash: "0de9cf2"
matlab_last_sync_hash: "124cf5e"
status: ported
python_path: "did/database.py"
python_class: "Database"
Expand Down Expand Up @@ -956,6 +956,17 @@ classes:
changes journal_mode. The rest of that commit is in sqlitedb.m; see
the sqlitedb entry.

DID-matlab a7ed185 (DID-matlab#235, 2026-10-24): query_struct_to_sql_str
matches depends_on with an anchored, case-sensitive instr on
meta.depends_on (';NAME,VALUE;', or ',VALUE;' for '*') instead of LIKE
"%NAME,VALUE;%", and search_doc_ids brute-forces (fieldsearch) a
depends_on whose name or value is empty or holds ',' / ';'. That is
what SQLiteDB's _depends_on_sql_clause already did (DID-python #101),
so nothing is ported; tests/test_depends_on_parity.py mirrors
DID-matlab's TestDependsOnParity case for case.
DID-matlab 124cf5e (same PR) only fixes how MATLAB's brute_force_search
builds its did.query; nothing to port.

# -----------------------------------------------------------------------
# did.document
# -----------------------------------------------------------------------
Expand Down Expand Up @@ -1454,7 +1465,7 @@ classes:
- name: query
type: class
matlab_path: "+did/query.m"
matlab_last_sync_hash: "3aa892d"
matlab_last_sync_hash: "a7ed185"
status: ported
python_path: "did/query.py"
python_class: "Query"
Expand Down Expand Up @@ -1549,6 +1560,11 @@ classes:
operations (regexp, exact_string, depends_on, isa, etc.)
are supported in Python. Synchronized 2026-03-15.

DID-matlab a7ed185 (DID-matlab#235) only adds help text to query.m:
depends_on's '*' is the only name wildcard, and name and value are
matched exactly and case-sensitively. Python's Query already resolves
depends_on that way; no code change.

# -----------------------------------------------------------------------
# did.ido
# -----------------------------------------------------------------------
Expand Down
6 changes: 5 additions & 1 deletion src/did/did_matlab_python_bridge_util.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ functions:
- name: fieldsearch
type: function
matlab_path: "+did/+datastructures/fieldsearch.m"
matlab_last_sync_hash: "0de9cf2"
matlab_last_sync_hash: "051f535"
status: ported
python_path: "did/datastructures.py"
python_name: "field_search"
Expand All @@ -326,6 +326,10 @@ functions:
case 'or' loops over did.datastructures.orOperands instead of recursing
once per nested OR, and stops at the first operand that matches;
field_search does the same with or_operands (DID-python #100 / #103).
DID-matlab 051f535 (DID-matlab#235): ~hasanysubfield_* on a field that
holds a non-struct value (depends_on = [] read back from a database)
used to return early, skipping the negation; it now matches. Python's
field_search already negated it; nothing to port.

- name: orOperands
type: function
Expand Down
122 changes: 122 additions & 0 deletions tests/test_depends_on_parity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""depends_on means the same thing on every search path, and in MATLAB.

``Query("", "depends_on", name, value)`` is "the document has a dependency
named ``name`` with value ``value``", where only the name ``'*'`` is a
wildcard (any name); matching is exact and case-sensitive. Each case below is
run on the SQLite search and on brute force (field_search), and both must
return the documents listed.

The same documents and cases are in DID-matlab's
tests/+did/+unittest/TestDependsOnParity.m. Keep the two identical, so that
the languages cannot drift apart again: DID-matlab's SQL search used to match
``LIKE "%name,value;%"``, under which an empty name matched any name, ``id``
also matched ``subject_id`` and ``ID`` matched ``id``.
"""

import os
import tempfile
import unittest
import uuid

from did.datastructures import field_search
from did.document import Document
from did.implementations.sqlitedb import SQLiteDB
from did.query import Query

# label -> [(dependency name, dependency value), ...]
DOCUMENTS = {
"A": [("subject_id", "X")],
"B": [("id", "X"), ("element_id", "Y")],
"C": [("ID", "X")],
"D": [],
"E": [("element_id", "x")],
}

# label -> (operation, name, value, expected documents)
CASES = {
"emptyName": ("depends_on", "", "X", []),
"starName": ("depends_on", "*", "X", ["A", "B", "C"]),
"exactName": ("depends_on", "id", "X", ["B"]),
"longerName": ("depends_on", "subject_id", "X", ["A"]),
"nameCase": ("depends_on", "ID", "X", ["C"]),
"valueCase": ("depends_on", "element_id", "X", []),
"valueCaseLower": ("depends_on", "element_id", "x", ["E"]),
"secondDependency": ("depends_on", "element_id", "Y", ["B"]),
"starSecondDependency": ("depends_on", "*", "Y", ["B"]),
"nameValueApart": ("depends_on", "id", "Y", []),
"starIsNotGlob": ("depends_on", "subject*", "X", []),
"noSuchValue": ("depends_on", "*", "Z", []),
"notExactName": ("~depends_on", "id", "X", ["A", "C", "D", "E"]),
"notStarName": ("~depends_on", "*", "X", ["D", "E"]),
"notEmptyName": ("~depends_on", "", "X", ["A", "B", "C", "D", "E"]),
"notNameCase": ("~depends_on", "ID", "X", ["A", "B", "D", "E"]),
}


def _doc(dependencies):
return Document(
{
"base": {
"id": uuid.uuid4().hex,
"session_id": "s1",
"name": "",
"datestamp": "2026-10-24T00:00:00.000Z",
},
"document_class": {"class_name": "demoC", "superclasses": []},
"depends_on": [{"name": n, "value": v} for n, v in dependencies],
}
)


class TestDependsOnParity(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.tmpdir = tempfile.TemporaryDirectory()
cls.db = SQLiteDB(os.path.join(cls.tmpdir.name, "depends_on_parity.sqlite"))
cls.db.add_branch("a")
cls.docs = {label: _doc(deps) for label, deps in DOCUMENTS.items()}
cls.db.add_docs(list(cls.docs.values()), branch_id="a", validate=False)

@classmethod
def tearDownClass(cls):
cls.db._close_db()
cls.tmpdir.cleanup()

def expected_ids(self, labels):
return sorted(self.docs[label].id() for label in labels)

def test_sqlite_search(self):
for label, (op, name, value, expected) in CASES.items():
with self.subTest(label):
found = self.db.search(Query("", op, name, value), branch_id="a")
self.assertEqual(sorted(found), self.expected_ids(expected))

def test_field_search(self):
for label, (op, name, value, expected) in CASES.items():
with self.subTest(label):
structs = Query("", op, name, value).to_search_structure()
found = [
doc.id()
for doc in self.docs.values()
if field_search(doc.document_properties, structs)
]
self.assertEqual(sorted(found), self.expected_ids(expected))

def test_sqlite_search_within_and(self):
# The same, as the second term of an AND: then it is checked against
# the documents the first term found only
for label, (op, name, value, expected) in CASES.items():
with self.subTest(label):
q = Query("", "isa", "demoC") & Query("", op, name, value)
found = self.db.search(q, branch_id="a")
self.assertEqual(sorted(found), self.expected_ids(expected))

def test_two_dependencies(self):
q = Query("", "depends_on", "id", "X") & Query(
"", "depends_on", "element_id", "Y"
)
self.assertEqual(self.db.search(q, branch_id="a"), [self.docs["B"].id()])


if __name__ == "__main__":
unittest.main()
Loading