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
|
# Verify batch was executed
|
||||||
mock_session.execute.assert_called()
|
mock_session.execute.assert_called()
|
||||||
|
|
||||||
def test_insert_literal_object_creates_3_entity_rows(self, entity_kg):
|
def test_insert_literal_object_creates_4_rows(self, entity_kg):
|
||||||
"""Test that inserting a quad with literal object creates 3 entity rows"""
|
"""Test that inserting a quad with literal object creates S+P+O entity rows + collection"""
|
||||||
kg, mock_session = entity_kg
|
kg, mock_session = entity_kg
|
||||||
|
|
||||||
mock_session.reset_mock()
|
mock_session.reset_mock()
|
||||||
|
|
|
||||||
|
|
@ -93,8 +93,8 @@ class TestWriteAmplification:
|
||||||
assert 'O' in roles
|
assert 'O' in roles
|
||||||
assert 'G' in roles
|
assert 'G' in roles
|
||||||
|
|
||||||
def test_literal_object_produces_3_entity_rows(self, entity_kg):
|
def test_literal_object_produces_4_rows(self, entity_kg):
|
||||||
"""Literal object → S + P entity rows (no O row) + collection row."""
|
"""Literal object → S + P + O entity rows + collection row."""
|
||||||
kg, ctx = entity_kg
|
kg, ctx = entity_kg
|
||||||
ctx["batches"].clear()
|
ctx["batches"].clear()
|
||||||
|
|
||||||
|
|
@ -109,13 +109,13 @@ class TestWriteAmplification:
|
||||||
|
|
||||||
batch = ctx["batches"][0]
|
batch = ctx["batches"][0]
|
||||||
|
|
||||||
# S + P entity rows + 1 collection = 3 (no O row for literal, no G for default)
|
# S + P + O entity rows + 1 collection = 4 (no G for default)
|
||||||
assert len(batch._adds) == 3
|
assert len(batch._adds) == 4
|
||||||
|
|
||||||
roles = [params[2] for _, params in batch._adds if len(params) == 10]
|
roles = [params[2] for _, params in batch._adds if len(params) == 10]
|
||||||
assert 'S' in roles
|
assert 'S' in roles
|
||||||
assert 'P' in roles
|
assert 'P' in roles
|
||||||
assert 'O' not in roles
|
assert 'O' in roles
|
||||||
assert 'G' not in roles
|
assert 'G' not in roles
|
||||||
|
|
||||||
def test_triple_otype_gets_object_entity_row(self, entity_kg):
|
def test_triple_otype_gets_object_entity_row(self, entity_kg):
|
||||||
|
|
@ -208,6 +208,129 @@ class TestWriteAmplification:
|
||||||
assert params[9] == 'en'
|
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
|
# In-partition filtering: get_os, get_spo
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -323,8 +446,8 @@ class TestDeleteCollectionBatching:
|
||||||
# Just verify the function completes and calls execute
|
# Just verify the function completes and calls execute
|
||||||
assert ctx["session"].execute.called
|
assert ctx["session"].execute.called
|
||||||
|
|
||||||
def test_literal_objects_not_treated_as_entities(self, entity_kg):
|
def test_literal_objects_treated_as_entities(self, entity_kg):
|
||||||
"""Literal objects (otype='l') should not get entity partition deletes."""
|
"""Literal objects (otype='l') should get entity partition deletes."""
|
||||||
kg, ctx = entity_kg
|
kg, ctx = entity_kg
|
||||||
|
|
||||||
mock_rows = [
|
mock_rows = [
|
||||||
|
|
@ -336,16 +459,15 @@ class TestDeleteCollectionBatching:
|
||||||
|
|
||||||
kg.delete_collection('col')
|
kg.delete_collection('col')
|
||||||
|
|
||||||
# Entity partition deletes should only include A and name, not Alice
|
|
||||||
entity_deletes = []
|
entity_deletes = []
|
||||||
for batch in ctx["batches"]:
|
for batch in ctx["batches"]:
|
||||||
for _, params in batch._adds:
|
for _, params in batch._adds:
|
||||||
if len(params) == 2: # delete_entity_partition takes (collection, entity)
|
if len(params) == 2:
|
||||||
entity_deletes.append(params[1])
|
entity_deletes.append(params[1])
|
||||||
|
|
||||||
assert 'http://ex.org/A' in entity_deletes
|
assert 'http://ex.org/A' in entity_deletes
|
||||||
assert 'http://ex.org/name' 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):
|
def test_non_default_graph_treated_as_entity(self, entity_kg):
|
||||||
"""Non-default graphs should get entity partition deletes."""
|
"""Non-default graphs should get entity partition deletes."""
|
||||||
|
|
|
||||||
|
|
@ -776,11 +776,10 @@ class EntityCentricKnowledgeGraph:
|
||||||
collection, p, 'P', p, otype, s, o, g, dtype, lang
|
collection, p, 'P', p, otype, s, o, g, dtype, lang
|
||||||
))
|
))
|
||||||
|
|
||||||
# Write row for object entity (role='O') - only for URIs, not literals
|
# Write row for object entity (role='O')
|
||||||
if otype == 'u' or otype == 't':
|
batch.add(self.insert_entity_stmt, (
|
||||||
batch.add(self.insert_entity_stmt, (
|
collection, o, 'O', p, otype, s, o, g, dtype, lang
|
||||||
collection, o, 'O', p, otype, s, o, g, dtype, lang
|
))
|
||||||
))
|
|
||||||
|
|
||||||
# Write row for graph entity (role='G') - only for non-default graphs
|
# Write row for graph entity (role='G') - only for non-default graphs
|
||||||
if g != DEFAULT_GRAPH:
|
if g != DEFAULT_GRAPH:
|
||||||
|
|
@ -997,15 +996,10 @@ class EntityCentricKnowledgeGraph:
|
||||||
lang = row.lang if hasattr(row, 'lang') else ''
|
lang = row.lang if hasattr(row, 'lang') else ''
|
||||||
quads.append((d, s, p, o, otype, dtype, lang))
|
quads.append((d, s, p, o, otype, dtype, lang))
|
||||||
|
|
||||||
# Subject and predicate are always entities
|
|
||||||
entities.add(s)
|
entities.add(s)
|
||||||
entities.add(p)
|
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:
|
if d != DEFAULT_GRAPH:
|
||||||
entities.add(d)
|
entities.add(d)
|
||||||
|
|
||||||
|
|
@ -1067,8 +1061,7 @@ class EntityCentricKnowledgeGraph:
|
||||||
batch = BatchStatement()
|
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, 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))
|
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:
|
if g != DEFAULT_GRAPH:
|
||||||
batch.add(self.insert_entity_stmt, (collection, g, 'G', p, otype, s, o, g, dtype, lang))
|
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))
|
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))
|
quads.append((d, s, p, o, otype, dtype, lang))
|
||||||
entities.add(s)
|
entities.add(s)
|
||||||
entities.add(p)
|
entities.add(p)
|
||||||
if otype == 'u' or otype == 't':
|
entities.add(o)
|
||||||
entities.add(o)
|
|
||||||
if d != DEFAULT_GRAPH:
|
if d != DEFAULT_GRAPH:
|
||||||
entities.add(d)
|
entities.add(d)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue