Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion packages/inputlayer-py/src/inputlayer/knowledge_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,8 +488,27 @@ def _add_agg_relations(agg: AggExpr) -> None:
elif isinstance(v, AggExpr):
ast_computed[k] = v
_add_agg_relations(v)
else:
elif isinstance(v, OrderedColumn):
raise CompileError(
f"{k}= is a sort direction, not a computed column",
hint=f"to sort, pass order_by= instead of {k}=",
)
elif isinstance(v, Expr):
ast_computed[k] = v
else:
# A keyword names a computed column; a value is not one.
is_column = any(
k in Relation._get_columns(r.relation_cls if isinstance(r, RelationRef) else r)
for r in relations
)
raise CompileError(
f"{k}={v!r} is not an expression: keyword arguments name computed columns",
hint=(
f"to filter on a value, pass where=lambda r: r.{k} == {v!r}"
if is_column
else "to filter, pass where=; to sort, pass order_by="
),
)

proxies = [
RelationProxy(
Expand Down
34 changes: 34 additions & 0 deletions packages/inputlayer-py/tests/docs_queries_live.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import asyncio
import contextlib
import os
import re
import time
from collections.abc import AsyncIterator
from typing import ClassVar
Expand Down Expand Up @@ -559,6 +560,39 @@ async def test_derived_relations_querying_a_recursive_rule(kg: KnowledgeGraph) -
assert sorted(reached) == [2, 3, 4]


def _proved(why) -> list[list]:
"""The conclusion of each row's proof tree, in row order."""
return [t.nodes[t.roots[0]].conclusion.args for t in why.proof_trees]


async def test_why_explains_each_row_of_a_derived_relation(kg: KnowledgeGraph) -> None:
await kg.define_rules(Reachable)
why = await kg.why(Reachable, where=lambda r: r.src == 1)
assert why.results.columns == ["src", "dst"]
assert sorted(why.results.to_tuples()) == [(1, 2), (1, 3), (1, 4)]
assert sorted(_proved(why)) == [[1, 2], [1, 3], [1, 4]]


async def test_why_orders_and_pages_a_derived_relation_with_its_proofs(kg: KnowledgeGraph) -> None:
await kg.define_rules(Reachable)
why = await kg.why(Reachable, order_by=Reachable.dst.desc(), limit=2)
assert [row[1] for row in why.results.to_tuples()] == [4, 4]
assert _proved(why) == [list(row) for row in why.results.to_tuples()]


async def test_why_explains_an_aggregate_over_a_derived_relation(kg: KnowledgeGraph) -> None:
await kg.define_rules(Reachable)
why = await kg.why(Reachable.src, count(Reachable.dst), join=[Reachable])
assert sorted(why.results.to_tuples()) == [(1, 3), (2, 2), (3, 1)]
assert _proved(why) == [list(row) for row in why.results.to_tuples()]


async def test_why_rejects_a_value_keyword_before_sending(kg: KnowledgeGraph) -> None:
await kg.define_rules(Reachable)
with pytest.raises(CompileError, match=re.escape("where=lambda r: r.src == 1")):
await kg.why(Reachable, src=1)


async def test_sessions_session_facts_mix_with_persistent_data(kg: KnowledgeGraph) -> None:
await kg.session.insert([
Employee(id=999, name="Temp", department="eng", salary=0.0, active=True),
Expand Down
36 changes: 36 additions & 0 deletions packages/inputlayer-py/tests/test_compile_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,3 +378,39 @@ async def test_vector_search_radius_text(self) -> None:
await kg.vector_search(Doc, [1.0, 0.0], radius=0.5)
(iql,), _ = kg._execute.await_args
assert iql == "?doc(Id, Embedding), Dist = cosine(Embedding, [1.0, 0.0]), Dist <= 0.5"


class TestKeywordArguments:
"""A keyword argument names a computed column; a plain value is an error."""

@pytest.mark.parametrize("method", ["query", "why", "debug"])
async def test_a_value_keyword_is_a_compile_error_naming_where(self, method) -> None:
kg = KnowledgeGraph("default", MagicMock())
kg._execute = AsyncMock()
with pytest.raises(CompileError) as exc:
await getattr(kg, method)(Employee, department="eng")
assert "department='eng'" in str(exc.value)
assert "where=lambda r: r.department == 'eng'" in (exc.value.hint or "")
kg._execute.assert_not_awaited()

@pytest.mark.parametrize("method", ["query", "why", "debug"])
async def test_a_sort_keyword_is_a_compile_error_naming_order_by(self, method) -> None:
kg = KnowledgeGraph("default", MagicMock())
kg._execute = AsyncMock()
with pytest.raises(CompileError) as exc:
await getattr(kg, method)(Employee, order=Employee.id.desc())
assert "order=" in str(exc.value)
assert "order_by=" in (exc.value.hint or "")
kg._execute.assert_not_awaited()

@pytest.mark.parametrize("method", ["query", "why", "debug"])
async def test_a_value_keyword_naming_no_column_gets_a_generic_hint(self, method) -> None:
kg = KnowledgeGraph("default", MagicMock())
kg._execute = AsyncMock()
with pytest.raises(CompileError) as exc:
await getattr(kg, method)(Employee, order="desc")
assert "order='desc'" in str(exc.value)
hint = exc.value.hint or ""
assert "r.order" not in hint
assert "where=" in hint and "order_by=" in hint
kg._execute.assert_not_awaited()
Loading