This commit is contained in:
Reid 'arrdem' McKenzie 2021-05-15 11:34:32 -06:00
commit bbae5ef63f
34 changed files with 956 additions and 886 deletions

View file

@ -9,17 +9,17 @@ from uuid import uuid4 as uuid
with open("graph.dtl", "w") as f:
nodes = []
nodes = []
# Generate 10k edges
for i in range(10000):
if nodes:
from_node = choice(nodes)
else:
from_node = uuid()
# Generate 10k edges
for i in range(10000):
if nodes:
from_node = choice(nodes)
else:
from_node = uuid()
to_node = uuid()
to_node = uuid()
nodes.append(to_node)
nodes.append(to_node)
f.write(f"edge({str(from_node)!r}, {str(to_node)!r}).\n")
f.write(f"edge({str(from_node)!r}, {str(to_node)!r}).\n")

View file

@ -26,5 +26,7 @@ setup(
],
# Package setup
package_dir={"": "src/python"},
packages=["datalog",],
packages=[
"datalog",
],
)

View file

@ -16,8 +16,8 @@ def constexpr_p(expr):
class Timing(object):
"""
A context manager object which records how long the context took.
"""
A context manager object which records how long the context took.
"""
def __init__(self):
self.start = None
@ -36,8 +36,8 @@ class Timing(object):
def __call__(self):
"""
If the context is exited, return its duration. Otherwise return the duration "so far".
"""
If the context is exited, return its duration. Otherwise return the duration "so far".
"""
from datetime import datetime

View file

@ -22,9 +22,9 @@ def read(text: str, db_cls=PartlyIndexedDataset):
def q(t: Tuple[str]) -> LTuple:
"""Helper for writing terse queries.
Takes a tuple of strings, and interprets them as a logic tuple.
So you don't have to write the logic tuple out by hand.
"""
Takes a tuple of strings, and interprets them as a logic tuple.
So you don't have to write the logic tuple out by hand.
"""
def _x(s: str):
if s[0].isupper():
@ -50,38 +50,38 @@ def __result(results_bindings):
def select(db: Dataset, query: Tuple[str], bindings=None) -> Sequence[Tuple]:
"""Helper for interpreting tuples of strings as a query, and returning simplified results.
Executes your query, returning matching full tuples.
"""
Executes your query, returning matching full tuples.
"""
return __mapv(__result, __select(db, q(query), bindings=bindings))
def join(db: Dataset, query: Sequence[Tuple[str]], bindings=None) -> Sequence[dict]:
"""Helper for interpreting a bunch of tuples of strings as a join query, and returning simplified
results.
results.
Executes the query clauses as a join, returning a sequence of tuples and binding mappings such
that the join constraints are simultaneously satisfied.
Executes the query clauses as a join, returning a sequence of tuples and binding mappings such
that the join constraints are simultaneously satisfied.
>>> db = read('''
... edge(a, b).
... edge(b, c).
... edge(c, d).
... ''')
>>> join(db, [
... ('edge', 'A', 'B'),
... ('edge', 'B', 'C')
... ])
[((('edge', 'a', 'b'),
('edge', 'b', 'c')),
{'A': 'a', 'B': 'b', 'C': 'c'}),
((('edge', 'b', 'c'),
('edge', 'c', 'd')),
{'A': 'b', 'B': 'c', 'C': 'd'}),
((('edge', 'c', 'd'),
('edge', 'd', 'f')),
{'A': 'c', 'B': 'd', 'C': 'f'})]
"""
>>> db = read('''
... edge(a, b).
... edge(b, c).
... edge(c, d).
... ''')
>>> join(db, [
... ('edge', 'A', 'B'),
... ('edge', 'B', 'C')
... ])
[((('edge', 'a', 'b'),
('edge', 'b', 'c')),
{'A': 'a', 'B': 'b', 'C': 'c'}),
((('edge', 'b', 'c'),
('edge', 'c', 'd')),
{'A': 'b', 'B': 'c', 'C': 'd'}),
((('edge', 'c', 'd'),
('edge', 'd', 'f')),
{'A': 'c', 'B': 'd', 'C': 'f'})]
"""
return __mapv(__result, __join(db, [q(c) for c in query], bindings=bindings))

View file

@ -20,8 +20,8 @@ from datalog.types import (
def match(tuple, expr, bindings=None):
"""Attempt to construct lvar bindings from expr such that tuple and expr equate.
If the match is successful, return the binding map, otherwise return None.
"""
If the match is successful, return the binding map, otherwise return None.
"""
bindings = bindings.copy() if bindings is not None else {}
for a, b in zip(expr, tuple):
@ -43,9 +43,9 @@ def match(tuple, expr, bindings=None):
def apply_bindings(expr, bindings, strict=True):
"""Given an expr which may contain lvars, substitute its lvars for constants returning the
simplified expr.
simplified expr.
"""
"""
if strict:
return tuple((bindings[e] if isinstance(e, LVar) else e) for e in expr)
@ -56,10 +56,10 @@ def apply_bindings(expr, bindings, strict=True):
def select(db: Dataset, expr, bindings=None, _recursion_guard=None, _select_guard=None):
"""Evaluate an expression in a database, lazily producing a sequence of 'matching' tuples.
The dataset is a set of tuples and rules, and the expression is a single tuple containing lvars
and constants. Evaluates rules and tuples, returning
The dataset is a set of tuples and rules, and the expression is a single tuple containing lvars
and constants. Evaluates rules and tuples, returning
"""
"""
def __select_tuples():
# As an opt. support indexed scans, which is optional.
@ -170,8 +170,8 @@ def select(db: Dataset, expr, bindings=None, _recursion_guard=None, _select_guar
def join(db: Dataset, clauses, bindings, pattern=None, _recursion_guard=None):
"""Evaluate clauses over the dataset, joining (or antijoining) with the seed bindings.
Yields a sequence of tuples and LVar bindings for which all joins and antijoins were satisfied.
"""
Yields a sequence of tuples and LVar bindings for which all joins and antijoins were satisfied.
"""
def __join(g, clause):
for ts, bindings in g:

View file

@ -27,13 +27,19 @@ class Actions(object):
return self._db_cls(tuples, rules)
def make_symbol(self, input, start, end, elements):
return LVar("".join(e.text for e in elements),)
return LVar(
"".join(e.text for e in elements),
)
def make_word(self, input, start, end, elements):
return Constant("".join(e.text for e in elements),)
return Constant(
"".join(e.text for e in elements),
)
def make_string(self, input, start, end, elements):
return Constant(elements[1].text,)
return Constant(
elements[1].text,
)
def make_comment(self, input, start, end, elements):
return None
@ -81,11 +87,11 @@ class Actions(object):
class Parser(Grammar):
"""Implementation detail.
A slightly hacked version of the Parser class canopy generates, which lets us control what the
parsing entry point is. This lets me play games with having one parser and one grammar which is
used both for the command shell and for other things.
A slightly hacked version of the Parser class canopy generates, which lets us control what the
parsing entry point is. This lets me play games with having one parser and one grammar which is
used both for the command shell and for other things.
"""
"""
def __init__(self, input, actions, types):
self._input = input

View file

@ -66,8 +66,8 @@ class Dataset(object):
class CachedDataset(Dataset):
"""An extension of the dataset which features a cache of rule produced tuples.
Note that this cache is lost when merging datasets - which ensures correctness.
"""
Note that this cache is lost when merging datasets - which ensures correctness.
"""
# Inherits tuples, rules, merge
@ -90,11 +90,11 @@ class CachedDataset(Dataset):
class TableIndexedDataset(CachedDataset):
"""An extension of the Dataset type which features both a cache and an index by table & length.
The index allows more efficient scans by maintaining 'table' style partitions.
It does not support user-defined indexing schemes.
The index allows more efficient scans by maintaining 'table' style partitions.
It does not support user-defined indexing schemes.
Note that index building is delayed until an index is scanned.
"""
Note that index building is delayed until an index is scanned.
"""
# From Dataset:
# tuples, rules, merge
@ -126,11 +126,11 @@ class TableIndexedDataset(CachedDataset):
class PartlyIndexedDataset(TableIndexedDataset):
"""An extension of the Dataset type which features both a cache and and a full index by table,
length, tuple index and value.
length, tuple index and value.
The index allows extremely efficient scans when elements of the tuple are known.
The index allows extremely efficient scans when elements of the tuple are known.
"""
"""
# From Dataset:
# tuples, rules, merge

View file

@ -25,8 +25,19 @@ def test_id_query(db_cls):
Constant("a"),
Constant("b"),
)
assert not select(db_cls([], []), ("a", "b",))
assert select(db_cls([ab], []), ("a", "b",)) == [((("a", "b"),), {},)]
assert not select(
db_cls([], []),
(
"a",
"b",
),
)
assert select(db_cls([ab], []), ("a", "b",)) == [
(
(("a", "b"),),
{},
)
]
@pytest.mark.parametrize("db_cls,", DBCLS)
@ -47,7 +58,17 @@ def test_lvar_unification(db_cls):
d = read("""edge(b, c). edge(c, c).""", db_cls=db_cls)
assert select(d, ("edge", "X", "X",)) == [((("edge", "c", "c"),), {"X": "c"})]
assert (
select(
d,
(
"edge",
"X",
"X",
),
)
== [((("edge", "c", "c"),), {"X": "c"})]
)
@pytest.mark.parametrize("db_cls,", DBCLS)
@ -105,12 +126,12 @@ no-b(X, Y) :-
def test_nested_antijoin(db_cls):
"""Test a query which negates a subquery which uses an antijoin.
Shouldn't exercise anything more than `test_antjoin` does, but it's an interesting case since you
actually can't capture the same semantics using a single query. Antijoins can't propagate positive
information (create lvar bindings) so I'm not sure you can express this another way without a
different evaluation strategy.
Shouldn't exercise anything more than `test_antjoin` does, but it's an interesting case since you
actually can't capture the same semantics using a single query. Antijoins can't propagate positive
information (create lvar bindings) so I'm not sure you can express this another way without a
different evaluation strategy.
"""
"""
d = read(
"""