diff --git a/terminusdb_client/tests/test_woqlQuery.py b/terminusdb_client/tests/test_woqlQuery.py index 1e973f9e..d281b130 100644 --- a/terminusdb_client/tests/test_woqlQuery.py +++ b/terminusdb_client/tests/test_woqlQuery.py @@ -15,7 +15,7 @@ from .woqljson.woqlConcatJson import WOQL_CONCAT_JSON from .woqljson.woqlIdgenJson import ( WOQL_IDGEN_JSON, - WOQL_RANDOM_IDGEN_JSON, + WOQL_RANDOM_KEY_JSON, WOQL_UNIQUE_JSON, ) from .woqljson.woqlJoinSplitJson import WOQL_JOIN_SPLIT_JSON @@ -214,11 +214,9 @@ def test_idgen_method(self): woql_object = WOQLQuery().idgen("Station", "v:Start_ID", "v:Start_Station_URL") assert woql_object.to_dict() == WOQL_IDGEN_JSON - def test_random_idgen_method(self): - woql_object = WOQLQuery().random_idgen( - "Station", "v:Start_ID", "v:Start_Station_URL" - ) - assert woql_object.to_dict() == WOQL_RANDOM_IDGEN_JSON + def test_idgen_random_method(self): + woql_object = WOQLQuery().idgen_random("Person/", "v:Person_ID") + assert woql_object.to_dict() == WOQL_RANDOM_KEY_JSON def test_typecast_method(self): woql_object = WOQLQuery().typecast( diff --git a/terminusdb_client/tests/test_woql_idgen_random.py b/terminusdb_client/tests/test_woql_idgen_random.py index e69de29b..bb11d169 100644 --- a/terminusdb_client/tests/test_woql_idgen_random.py +++ b/terminusdb_client/tests/test_woql_idgen_random.py @@ -0,0 +1,122 @@ +"""Unit tests for idgen_random WOQL method""" +from terminusdb_client.woqlquery.woql_query import WOQLQuery + + +class TestIdgenRandom: + """Test idgen_random WOQL method""" + + def test_idgen_random_basic(self): + """Test basic idgen_random functionality""" + woql = WOQLQuery().idgen_random("Person/", "v:PersonID") + result = woql.to_dict() + + assert result["@type"] == "RandomKey" + assert result["base"]["@type"] == "DataValue" + assert result["base"]["data"]["@type"] == "xsd:string" + assert result["base"]["data"]["@value"] == "Person/" + assert result["uri"]["@type"] == "NodeValue" + assert result["uri"]["variable"] == "PersonID" + + def test_idgen_random_with_prefix(self): + """Test idgen_random with different prefix formats""" + woql = WOQLQuery().idgen_random("http://example.org/Person/", "v:ID") + result = woql.to_dict() + + assert result["base"]["data"]["@value"] == "http://example.org/Person/" + assert result["uri"]["variable"] == "ID" + + def test_idgen_random_chaining(self): + """Test idgen_random can be chained with other operations""" + woql = (WOQLQuery() + .triple("v:Person", "rdf:type", "@schema:Person") + .idgen_random("Person/", "v:PersonID")) + + result = woql.to_dict() + assert result["@type"] == "And" + assert len(result["and"]) == 2 + assert result["and"][0]["@type"] == "Triple" + assert result["and"][1]["@type"] == "RandomKey" + + def test_idgen_random_multiple_calls(self): + """Test multiple idgen_random calls in same query""" + woql = (WOQLQuery() + .idgen_random("Person/", "v:PersonID") + .idgen_random("Order/", "v:OrderID")) + + result = woql.to_dict() + assert result["@type"] == "And" + assert result["and"][0]["@type"] == "RandomKey" + assert result["and"][0]["base"]["data"]["@value"] == "Person/" + assert result["and"][1]["@type"] == "RandomKey" + assert result["and"][1]["base"]["data"]["@value"] == "Order/" + + def test_idgen_random_args_parameter(self): + """Test idgen_random args parameter returns parameter list""" + result = WOQLQuery().idgen_random("args", "v:ID") + + assert result == ["base", "uri"] + + def test_idgen_random_empty_prefix(self): + """Test idgen_random with empty prefix""" + woql = WOQLQuery().idgen_random("", "v:ID") + result = woql.to_dict() + + assert result["@type"] == "RandomKey" + assert result["base"]["data"]["@value"] == "" + + def test_idgen_random_variable_output(self): + """Test idgen_random output variable format""" + woql = WOQLQuery().idgen_random("Test/", "v:MyVar") + result = woql.to_dict() + + assert result["uri"]["variable"] == "MyVar" + + def test_idgen_random_in_query_chain(self): + """Test idgen_random in complex query chain""" + woql = (WOQLQuery() + .triple("v:Person", "rdf:type", "@schema:Person") + .idgen_random("Person/", "v:PersonID") + .triple("v:PersonID", "@schema:name", "v:Name")) + + result = woql.to_dict() + assert result["@type"] == "And" + # WOQLQuery chains create nested And structures + # Verify RandomKey is present in the chain + has_random_key = False + + def check_for_random_key(obj): + nonlocal has_random_key + if isinstance(obj, dict): + if obj.get("@type") == "RandomKey": + has_random_key = True + for value in obj.values(): + check_for_random_key(value) + elif isinstance(obj, list): + for item in obj: + check_for_random_key(item) + + check_for_random_key(result) + assert has_random_key, "RandomKey should be present in query chain" + + def test_idgen_random_matches_expected_json(self): + """Test idgen_random produces expected WOQL JSON structure""" + from terminusdb_client.tests.woqljson.woqlIdgenJson import WOQL_RANDOM_KEY_JSON + + woql = WOQLQuery().idgen_random("Person/", "v:Person_ID") + result = woql.to_dict() + + assert result == WOQL_RANDOM_KEY_JSON + + def test_idgen_random_with_special_characters_in_prefix(self): + """Test idgen_random handles special characters in prefix""" + woql = WOQLQuery().idgen_random("Test/2024-11/", "v:ID") + result = woql.to_dict() + + assert result["base"]["data"]["@value"] == "Test/2024-11/" + + def test_idgen_random_preserves_cursor_state(self): + """Test idgen_random returns self for method chaining""" + woql = WOQLQuery() + result = woql.idgen_random("Test/", "v:ID") + + assert result is woql # Should return same object for chaining diff --git a/terminusdb_client/tests/woqljson/woqlIdgenJson.py b/terminusdb_client/tests/woqljson/woqlIdgenJson.py index e539b302..5a09ecb8 100644 --- a/terminusdb_client/tests/woqljson/woqlIdgenJson.py +++ b/terminusdb_client/tests/woqljson/woqlIdgenJson.py @@ -27,18 +27,14 @@ }, } -WOQL_RANDOM_IDGEN_JSON = { +WOQL_RANDOM_KEY_JSON = { "@type": "RandomKey", "base": { "@type": "DataValue", - "data": {"@type": "xsd:string", "@value": "Station"}, - }, - "key_list": { - "@type": "DataValue", - "variable": "Start_ID", + "data": {"@type": "xsd:string", "@value": "Person/"}, }, "uri": { "@type": "NodeValue", - "variable": "Start_Station_URL", + "variable": "Person_ID", }, } diff --git a/terminusdb_client/woqlquery/woql_query.py b/terminusdb_client/woqlquery/woql_query.py index 35fbad21..d54ea363 100644 --- a/terminusdb_client/woqlquery/woql_query.py +++ b/terminusdb_client/woqlquery/woql_query.py @@ -2207,38 +2207,65 @@ def idgen(self, prefix, input_var_list, output_var): self._cursor["uri"] = self._clean_node_value(output_var) return self - def random_idgen(self, prefix, key_list, uri): - """Randomly generates an ID and appends to the end of the key_list. + def idgen_random(self, prefix, uri): + """Generates a unique ID with cryptographically secure random suffix. + + Uses base64 encoding to generate 16-character random IDs that are + guaranteed to be unique across executions. Matches the server's + idgen_random/2 predicate and maintains consistency with the JavaScript client. Parameters ---------- prefix : str - prefix for the id - key_list : str - variable to generate id for + A prefix for the IDs to be generated (e.g. "Person/") uri : str - the variable to hold the id + Variable name or output target for the generated ID Returns ------- WOQLQuery object - query object that can be chained and/or execute + query object that can be chained and/or executed Examples ------- - >>> WOQLQuery().random_idgen("https://base.url",["page","1"],"v:obj_id").execute(client) - {'@type': 'api:WoqlResponse', 'api:status': 'api:success', 'api:variable_names': ['obj_id'], 'bindings': [{'obj_id': 'http://base.url_page_1_rv1mfa59ekisdutnxx6zdt2fkockgah'}], 'deletes': 0, 'inserts': 0, 'transaction_retry_count': 0} + >>> WOQLQuery().idgen_random("Person/", "v:person_id").execute(client) + {'@type': 'api:WoqlResponse', 'api:status': 'api:success', 'api:variable_names': ['person_id'], 'bindings': [{'person_id': 'Person/aB3dEf9GhI2jK4lM'}], 'deletes': 0, 'inserts': 0, 'transaction_retry_count': 0} """ if prefix and prefix == "args": - return ["base", "key_list", "uri"] + return ["base", "uri"] if self._cursor.get("@type"): self._wrap_cursor_with_and() self._cursor["@type"] = "RandomKey" self._cursor["base"] = self._clean_data_value(prefix) - self._cursor["key_list"] = self._data_list(key_list) self._cursor["uri"] = self._clean_node_value(uri) return self + def random_idgen(self, prefix, uri): + """Generates a unique ID with cryptographically secure random suffix (alias for idgen_random). + + This is an alias for idgen_random() for compatibility with older code. + Uses base64 encoding to generate 16-character random IDs that are + guaranteed to be unique across executions. + + Parameters + ---------- + prefix : str + A prefix for the IDs to be generated (e.g. "Person/") + uri : str + Variable name or output target for the generated ID + + Returns + ------- + WOQLQuery object + query object that can be chained and/or executed + + Examples + ------- + >>> WOQLQuery().random_idgen("Person/", "v:person_id").execute(client) + {'@type': 'api:WoqlResponse', 'api:status': 'api:success', 'api:variable_names': ['person_id'], 'bindings': [{'person_id': 'Person/aB3dEf9GhI2jK4lM'}], 'deletes': 0, 'inserts': 0, 'transaction_retry_count': 0} + """ + return self.idgen_random(prefix, uri) + def upper(self, left, right): """Changes a string to upper-case - input is in left, output in right