mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-14 07:42:11 +02:00
fix: index literal objects in entity table for object-based graph queries (#1042)
Literal objects (e.g. rdfs:label values) were not being inserted into the quads_by_entity table with role='O', so queries like `tg-query-graph -o MIL-HDBK-516D` returned no results even though the triple existed. Removed the otype guard that skipped entity row insertion for literals in both insert() and async_insert(). Also updated delete_collection() and async_delete_collection() to include literal objects in entity partition cleanup. Existing data requires re-ingestion for old literal objects to become queryable by object value.
This commit is contained in:
parent
35fc8444ec
commit
f9f5a2318f
3 changed files with 141 additions and 27 deletions
|
|
@ -99,8 +99,8 @@ class TestEntityCentricKnowledgeGraph:
|
|||
# Verify batch was executed
|
||||
mock_session.execute.assert_called()
|
||||
|
||||
def test_insert_literal_object_creates_3_entity_rows(self, entity_kg):
|
||||
"""Test that inserting a quad with literal object creates 3 entity rows"""
|
||||
def test_insert_literal_object_creates_4_rows(self, entity_kg):
|
||||
"""Test that inserting a quad with literal object creates S+P+O entity rows + collection"""
|
||||
kg, mock_session = entity_kg
|
||||
|
||||
mock_session.reset_mock()
|
||||
|
|
|
|||
|
|
@ -93,8 +93,8 @@ class TestWriteAmplification:
|
|||
assert 'O' in roles
|
||||
assert 'G' in roles
|
||||
|
||||
def test_literal_object_produces_3_entity_rows(self, entity_kg):
|
||||
"""Literal object → S + P entity rows (no O row) + collection row."""
|
||||
def test_literal_object_produces_4_rows(self, entity_kg):
|
||||
"""Literal object → S + P + O entity rows + collection row."""
|
||||
kg, ctx = entity_kg
|
||||
ctx["batches"].clear()
|
||||
|
||||
|
|
@ -109,13 +109,13 @@ class TestWriteAmplification:
|
|||
|
||||
batch = ctx["batches"][0]
|
||||
|
||||
# S + P entity rows + 1 collection = 3 (no O row for literal, no G for default)
|
||||
assert len(batch._adds) == 3
|
||||
# S + P + O entity rows + 1 collection = 4 (no G for default)
|
||||
assert len(batch._adds) == 4
|
||||
|
||||
roles = [params[2] for _, params in batch._adds if len(params) == 10]
|
||||
assert 'S' in roles
|
||||
assert 'P' in roles
|
||||
assert 'O' not in roles
|
||||
assert 'O' in roles
|
||||
assert 'G' not in roles
|
||||
|
||||
def test_triple_otype_gets_object_entity_row(self, entity_kg):
|
||||
|
|
@ -208,6 +208,129 @@ class TestWriteAmplification:
|
|||
assert params[9] == 'en'
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Literal object queryability: insert must index literals as entities
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestLiteralObjectQueryability:
|
||||
|
||||
def test_literal_object_gets_entity_row_for_query(self, entity_kg):
|
||||
"""Literal objects must get an O entity row so get_o() can find them.
|
||||
|
||||
Regression: inserting (s, rdfs:label, "MIL-HDBK-516D") then calling
|
||||
get_o("MIL-HDBK-516D") returned nothing because no role='O' row was
|
||||
written for literals.
|
||||
"""
|
||||
kg, ctx = entity_kg
|
||||
ctx["batches"].clear()
|
||||
|
||||
kg.insert(
|
||||
collection='col',
|
||||
s='http://trustgraph.ai/e/mil-hdbk-516d',
|
||||
p='http://www.w3.org/2000/01/rdf-schema#label',
|
||||
o='MIL-HDBK-516D',
|
||||
g=None,
|
||||
otype='l',
|
||||
)
|
||||
|
||||
batch = ctx["batches"][0]
|
||||
roles = [params[2] for _, params in batch._adds if len(params) == 10]
|
||||
|
||||
assert 'O' in roles, "Literal objects must have role='O' entity row for get_o() queries"
|
||||
|
||||
o_rows = [
|
||||
params for _, params in batch._adds
|
||||
if len(params) == 10 and params[2] == 'O'
|
||||
]
|
||||
assert len(o_rows) == 1
|
||||
assert o_rows[0][1] == 'MIL-HDBK-516D' # entity = object value
|
||||
assert o_rows[0][4] == 'l' # otype preserved
|
||||
|
||||
def test_literal_object_with_dtype_and_lang_gets_entity_row(self, entity_kg):
|
||||
"""Literal with datatype and language tag must also be indexed."""
|
||||
kg, ctx = entity_kg
|
||||
ctx["batches"].clear()
|
||||
|
||||
kg.insert(
|
||||
collection='col',
|
||||
s='http://ex.org/Alice',
|
||||
p='http://www.w3.org/2000/01/rdf-schema#label',
|
||||
o='Alice Smith',
|
||||
g=None,
|
||||
otype='l',
|
||||
dtype='xsd:string',
|
||||
lang='en',
|
||||
)
|
||||
|
||||
batch = ctx["batches"][0]
|
||||
roles = [params[2] for _, params in batch._adds if len(params) == 10]
|
||||
|
||||
assert 'O' in roles
|
||||
|
||||
o_rows = [
|
||||
params for _, params in batch._adds
|
||||
if len(params) == 10 and params[2] == 'O'
|
||||
]
|
||||
assert o_rows[0][8] == 'xsd:string' # dtype
|
||||
assert o_rows[0][9] == 'en' # lang
|
||||
|
||||
def test_literal_object_default_graph_produces_4_rows(self, entity_kg):
|
||||
"""Literal + default graph → S + P + O entity rows + 1 collection = 4."""
|
||||
kg, ctx = entity_kg
|
||||
ctx["batches"].clear()
|
||||
|
||||
kg.insert(
|
||||
collection='col',
|
||||
s='http://ex.org/s',
|
||||
p='http://ex.org/label',
|
||||
o='some label',
|
||||
g=None,
|
||||
otype='l',
|
||||
)
|
||||
|
||||
batch = ctx["batches"][0]
|
||||
assert len(batch._adds) == 4 # S + P + O + collection
|
||||
|
||||
def test_literal_object_non_default_graph_produces_5_rows(self, entity_kg):
|
||||
"""Literal + named graph → S + P + O + G entity rows + collection = 5."""
|
||||
kg, ctx = entity_kg
|
||||
ctx["batches"].clear()
|
||||
|
||||
kg.insert(
|
||||
collection='col',
|
||||
s='http://ex.org/s',
|
||||
p='http://ex.org/label',
|
||||
o='some label',
|
||||
g='http://ex.org/g1',
|
||||
otype='l',
|
||||
)
|
||||
|
||||
batch = ctx["batches"][0]
|
||||
assert len(batch._adds) == 5 # S + P + O + G + collection
|
||||
|
||||
def test_delete_collection_includes_literal_object_entities(self, entity_kg):
|
||||
"""Literal objects must be included in entity partition deletes."""
|
||||
kg, ctx = entity_kg
|
||||
|
||||
mock_rows = [
|
||||
MagicMock(d='', s='http://ex.org/A', p='http://ex.org/name',
|
||||
o='Alice', otype='l', dtype='', lang=''),
|
||||
]
|
||||
ctx["session"].execute.return_value = mock_rows
|
||||
ctx["batches"].clear()
|
||||
|
||||
kg.delete_collection('col')
|
||||
|
||||
entity_deletes = []
|
||||
for batch in ctx["batches"]:
|
||||
for _, params in batch._adds:
|
||||
if len(params) == 2:
|
||||
entity_deletes.append(params[1])
|
||||
|
||||
assert 'Alice' in entity_deletes, \
|
||||
"Literal object entities must be deleted when collection is deleted"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# In-partition filtering: get_os, get_spo
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -323,8 +446,8 @@ class TestDeleteCollectionBatching:
|
|||
# Just verify the function completes and calls execute
|
||||
assert ctx["session"].execute.called
|
||||
|
||||
def test_literal_objects_not_treated_as_entities(self, entity_kg):
|
||||
"""Literal objects (otype='l') should not get entity partition deletes."""
|
||||
def test_literal_objects_treated_as_entities(self, entity_kg):
|
||||
"""Literal objects (otype='l') should get entity partition deletes."""
|
||||
kg, ctx = entity_kg
|
||||
|
||||
mock_rows = [
|
||||
|
|
@ -336,16 +459,15 @@ class TestDeleteCollectionBatching:
|
|||
|
||||
kg.delete_collection('col')
|
||||
|
||||
# Entity partition deletes should only include A and name, not Alice
|
||||
entity_deletes = []
|
||||
for batch in ctx["batches"]:
|
||||
for _, params in batch._adds:
|
||||
if len(params) == 2: # delete_entity_partition takes (collection, entity)
|
||||
if len(params) == 2:
|
||||
entity_deletes.append(params[1])
|
||||
|
||||
assert 'http://ex.org/A' in entity_deletes
|
||||
assert 'http://ex.org/name' in entity_deletes
|
||||
assert 'Alice' not in entity_deletes
|
||||
assert 'Alice' in entity_deletes
|
||||
|
||||
def test_non_default_graph_treated_as_entity(self, entity_kg):
|
||||
"""Non-default graphs should get entity partition deletes."""
|
||||
|
|
|
|||
|
|
@ -776,11 +776,10 @@ class EntityCentricKnowledgeGraph:
|
|||
collection, p, 'P', p, otype, s, o, g, dtype, lang
|
||||
))
|
||||
|
||||
# Write row for object entity (role='O') - only for URIs, not literals
|
||||
if otype == 'u' or otype == 't':
|
||||
batch.add(self.insert_entity_stmt, (
|
||||
collection, o, 'O', p, otype, s, o, g, dtype, lang
|
||||
))
|
||||
# Write row for object entity (role='O')
|
||||
batch.add(self.insert_entity_stmt, (
|
||||
collection, o, 'O', p, otype, s, o, g, dtype, lang
|
||||
))
|
||||
|
||||
# Write row for graph entity (role='G') - only for non-default graphs
|
||||
if g != DEFAULT_GRAPH:
|
||||
|
|
@ -997,15 +996,10 @@ class EntityCentricKnowledgeGraph:
|
|||
lang = row.lang if hasattr(row, 'lang') else ''
|
||||
quads.append((d, s, p, o, otype, dtype, lang))
|
||||
|
||||
# Subject and predicate are always entities
|
||||
entities.add(s)
|
||||
entities.add(p)
|
||||
entities.add(o)
|
||||
|
||||
# Object is an entity only for URIs
|
||||
if otype == 'u' or otype == 't':
|
||||
entities.add(o)
|
||||
|
||||
# Graph is an entity for non-default graphs
|
||||
if d != DEFAULT_GRAPH:
|
||||
entities.add(d)
|
||||
|
||||
|
|
@ -1067,8 +1061,7 @@ class EntityCentricKnowledgeGraph:
|
|||
batch = BatchStatement()
|
||||
batch.add(self.insert_entity_stmt, (collection, s, 'S', p, otype, s, o, g, dtype, lang))
|
||||
batch.add(self.insert_entity_stmt, (collection, p, 'P', p, otype, s, o, g, dtype, lang))
|
||||
if otype == 'u' or otype == 't':
|
||||
batch.add(self.insert_entity_stmt, (collection, o, 'O', p, otype, s, o, g, dtype, lang))
|
||||
batch.add(self.insert_entity_stmt, (collection, o, 'O', p, otype, s, o, g, dtype, lang))
|
||||
if g != DEFAULT_GRAPH:
|
||||
batch.add(self.insert_entity_stmt, (collection, g, 'G', p, otype, s, o, g, dtype, lang))
|
||||
batch.add(self.insert_collection_stmt, (collection, g, s, p, o, otype, dtype, lang))
|
||||
|
|
@ -1233,8 +1226,7 @@ class EntityCentricKnowledgeGraph:
|
|||
quads.append((d, s, p, o, otype, dtype, lang))
|
||||
entities.add(s)
|
||||
entities.add(p)
|
||||
if otype == 'u' or otype == 't':
|
||||
entities.add(o)
|
||||
entities.add(o)
|
||||
if d != DEFAULT_GRAPH:
|
||||
entities.add(d)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue