diff --git a/terminusdb_client/tests/integration_tests/test_client.py b/terminusdb_client/tests/integration_tests/test_client.py index e87caa0e..dae9fcea 100644 --- a/terminusdb_client/tests/integration_tests/test_client.py +++ b/terminusdb_client/tests/integration_tests/test_client.py @@ -222,6 +222,53 @@ def test_woql_substr(docker_url): assert result["bindings"][0]["Substring"]["@value"] == "Soc" +def test_woql_triple_relation(docker_url): + client = Client(docker_url, user_agent=test_user_agent) + client.connect() + db_name = "relations" + str(random()) + client.create_database(db_name) + client.connect(db=db_name) + schema = { + "@type": "Class", + "@id": "Event", + "interval": "xdd:dateTimeInterval", + } + client.insert_document(schema, graph_type="schema") + client.insert_document( + [ + { + "@type": "Event", + "@id": "Event/ev-2025-Q1", + "interval": { + "@type": "xdd:dateTimeInterval", + "@value": "2025-01-01T00:00:00Z/2025-04-01T00:00:00Z", + }, + }, + { + "@type": "Event", + "@id": "Event/ev-2025-Q2", + "interval": { + "@type": "xdd:dateTimeInterval", + "@value": "2025-04-01T00:00:00Z/2025-07-01T00:00:00Z", + }, + }, + ] + ) + result = client.query( + WOQLQuery().triple_relation( + "v:doc", + "@schema:interval", + "v:iv", + "meets", + { + "@type": "xdd:dateTimeInterval", + "@value": "2025-04-01T00:00:00Z/2025-07-01T00:00:00Z", + }, + ) + ) + assert [b["doc"] for b in result["bindings"]] == ["Event/ev-2025-Q1"] + + def test_diff_apply_version(docker_url): client = Client(docker_url, user_agent=test_user_agent) client.connect() diff --git a/terminusdb_client/tests/test_woqlQuery.py b/terminusdb_client/tests/test_woqlQuery.py index 7c6594d1..65cc0602 100644 --- a/terminusdb_client/tests/test_woqlQuery.py +++ b/terminusdb_client/tests/test_woqlQuery.py @@ -67,6 +67,33 @@ def test_woql_not_method(self): assert woql_object.to_dict() == json_obj assert woql_object_chain.to_dict() == json_obj + def test_triple_relation_method(self): + woql_object = WOQLQuery().triple_relation( + "v:doc", + "scm:interval", + "v:iv", + "meets", + {"@type": "xdd:dateTimeInterval", "@value": "2025-01-01/2025-04-01"}, + ) + json_obj = { + "@type": "TripleRelation", + "subject": {"@type": "NodeValue", "variable": "doc"}, + "predicate": {"@type": "NodeValue", "node": "scm:interval"}, + "object": {"@type": "Value", "variable": "iv"}, + "relation": { + "@type": "DataValue", + "data": {"@type": "xsd:string", "@value": "meets"}, + }, + "value": { + "@type": "DataValue", + "data": { + "@type": "xdd:dateTimeInterval", + "@value": "2025-01-01/2025-04-01", + }, + }, + } + assert woql_object.to_dict() == json_obj + def test_woql_and_method(self): woql_object = WOQLQuery().woql_and( WOQLQuery().triple("a", "b", "c"), WOQLQuery().triple("1", "2", "3") @@ -237,6 +264,36 @@ def test_cast_method(self): woql_object = WOQLQuery().cast("v:Duration", "xsd:integer", "v:Duration_Cast") assert woql_object.to_dict() == WOQL_CAST_JSON + def test_cast_method_prefixes_dict(self): + woql_object = WOQLQuery().cast( + "v:QName_str", + "xsd:QName", + "v:QName_val", + prefixes={"eg": "http://example.org/ns#"}, + ) + json_obj = woql_object.to_dict() + assert json_obj["prefixes"] == { + "@type": "Value", + "data": {"eg": "http://example.org/ns#"}, + } + + def test_cast_method_prefixes_var(self): + woql_object = WOQLQuery().cast( + "v:QName_str", + "xsd:QName", + "v:QName_val", + prefixes="v:Namespaces", + ) + json_obj = woql_object.to_dict() + assert json_obj["prefixes"] == { + "@type": "Value", + "variable": "Namespaces", + } + + def test_cast_method_no_prefixes(self): + woql_object = WOQLQuery().cast("v:Duration", "xsd:integer", "v:Duration_Cast") + assert "prefixes" not in woql_object.to_dict() + def test_cast_method_literal(self): woql_object1 = WOQLQuery().cast( "1", "xsd:integer", "v:Duration_Cast", literal_type="integer" diff --git a/terminusdb_client/woqlquery/woql_query.py b/terminusdb_client/woqlquery/woql_query.py index 8aed3e67..0da3d809 100644 --- a/terminusdb_client/woqlquery/woql_query.py +++ b/terminusdb_client/woqlquery/woql_query.py @@ -2715,6 +2715,45 @@ def interval_relation_typed(self, relation, x, y): self._cursor["y"] = self._clean_object(y) return self + def triple_relation(self, sub, pred, obj, relation, value): + """Finds triples whose object stands in a typed relation to a + reference value. + + Supports the thirteen Allen interval relations on + xdd:dateTimeInterval objects; instant objects (xsd:dateTime) + compare as zero-duration points. An instant at a meeting boundary + belongs to the interval it starts. + + Parameters + ---------- + sub : str + Subject, has to be a node (URI) or variable + pred : str + Predicate, can be variable (prefix with "v:") or node + obj : str or dict + The interval or instant object or a variable + relation : str or dict + relation name (e.g. "during", "meets") + value : str or dict + the reference interval or instant literal + + Returns + ------- + WOQLQuery object + query object that can be chained and/or execute + """ + if relation is None or value is None: + raise ValueError("TripleRelation takes five parameters") + if self._cursor.get("@type"): + self._wrap_cursor_with_and() + self._cursor["@type"] = "TripleRelation" + self._cursor["subject"] = self._clean_subject(sub) + self._cursor["predicate"] = self._clean_predicate(pred) + self._cursor["object"] = self._clean_object(obj) + self._cursor["relation"] = self._clean_data_value(relation) + self._cursor["value"] = self._clean_data_value(value) + return self + def range_min(self, input_list, result): """Find the minimum value in a list using the standard ordering. @@ -3766,7 +3805,7 @@ def count(self, countvar, query=None): self._cursor["count"] = self._clean_data_value(countvar) return self._add_sub_query(query) - def cast(self, val, user_type, result, literal_type=None): + def cast(self, val, user_type, result, literal_type=None, prefixes=None): """Changes the type of va to type and saves the return in vb Parameters @@ -3780,6 +3819,11 @@ def cast(self, val, user_type, result, literal_type=None): literal_type: str, optional literal type of`val`, can be used to treat `val` as a literal rather than an object or variable in the WOQL query. If literal type is "owl:Thing" or "node", `val` will be treated as object in the graph + prefixes : dict or str or Var, optional + prefix map used to expand or contract expanded names + (xsd:QName, xbrl:SQName) during the cast. A plain dict is sent + as a literal map; a "v:" string or Var is a variable bound + elsewhere (e.g. via dot on a stored namespaces dict). Returns ------- @@ -3790,9 +3834,13 @@ def cast(self, val, user_type, result, literal_type=None): if (literal_type == "owl:Thing" or literal_type == "node") and isinstance( val, str ): - return self.cast({"@type": "Value", "node": val}, user_type, result) + return self.cast( + {"@type": "Value", "node": val}, user_type, result, prefixes=prefixes + ) else: - return self.cast(self.literal(val, literal_type), user_type, result) + return self.cast( + self.literal(val, literal_type), user_type, result, prefixes=prefixes + ) if val and val == "args": return ["value", "type", "result"] if self._cursor.get("@type"): @@ -3801,6 +3849,11 @@ def cast(self, val, user_type, result, literal_type=None): self._cursor["value"] = self._clean_object(val) self._cursor["type"] = self._clean_node_value(user_type) self._cursor["result"] = self._clean_object(result) + if prefixes is not None: + if type(prefixes) is dict and "@type" not in prefixes: + self._cursor["prefixes"] = {"@type": "Value", "data": prefixes} + else: + self._cursor["prefixes"] = self._clean_object(prefixes) return self def type_of(self, value, vtype):