From 208ffe1c73cbab570fc91fb4381068c2b300c61b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 18:54:32 +0000 Subject: [PATCH] Test that depends_on matches as in DID-matlab, case for case tests/test_depends_on_parity.py runs the same five documents and sixteen cases as DID-matlab's new TestDependsOnParity on the SQLite search, on field_search and within an AND: an empty name matches nothing, '*' any name, a name exactly (not a suffix of another, not another case), only '*' is a wildcard, and the negated form is the complement. DID-python main already passes all of them; DID-matlab's SQL search did not until DID-matlab#235. Bridge: database and query re-pointed at DID-matlab a7ed185, with the decision that nothing needs porting (SQLiteDB's _depends_on_sql_clause already matches this way; query.m only gained help text). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014BLjcMbQRXr5MDdivxzYEc --- src/did/did_matlab_python_bridge.yaml | 20 +++- src/did/did_matlab_python_bridge_util.yaml | 6 +- tests/test_depends_on_parity.py | 122 +++++++++++++++++++++ 3 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 tests/test_depends_on_parity.py diff --git a/src/did/did_matlab_python_bridge.yaml b/src/did/did_matlab_python_bridge.yaml index 510d754..eef731a 100644 --- a/src/did/did_matlab_python_bridge.yaml +++ b/src/did/did_matlab_python_bridge.yaml @@ -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" @@ -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 # ----------------------------------------------------------------------- @@ -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" @@ -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 # ----------------------------------------------------------------------- diff --git a/src/did/did_matlab_python_bridge_util.yaml b/src/did/did_matlab_python_bridge_util.yaml index 056d5d8..41ddc42 100644 --- a/src/did/did_matlab_python_bridge_util.yaml +++ b/src/did/did_matlab_python_bridge_util.yaml @@ -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" @@ -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 diff --git a/tests/test_depends_on_parity.py b/tests/test_depends_on_parity.py new file mode 100644 index 0000000..a3ee04c --- /dev/null +++ b/tests/test_depends_on_parity.py @@ -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()