Package: arrdem.datalog
DatalogAn implementation of Datalog in Python.What is Datalog?Datalogis a fully declarative language for expressing relational data and queries, typically written using a syntactic subset of Prolog.
Its most interesting feature compared to other relational languages such as SQL is that it features production rules.Briefly, a datalog database consists of rules and tuples.Tuples are writtena(b, "c", 126, ...)., require no declaration eg.
of table, may be of arbitrary even varying length.Rules are writtenf(A, B) :- a(A), b(B)and when evaluated produce tuples.
This rule for instance would define∀ aₑ∈a, bₑ∈b f(a, b)eg the cross-product of the elements of the tuple setsa(A)andb(B).QuickstartWe're gonna make use of thedatalog.easyAPI.
It's somewhat simplified and definitely has sharp edges, but has much better ergonomics than working directly with the query engine from Python.# Pull in the datalog.easy package
>>> from datalog import easy
# Read some tuples into a Dataset.
#
# Because the base Dataset class has some limitations, easy gives you
# an instance of the IndexedDataset which is best supported
>>> db = read('''
... edge(a, b).
... edge(b, c).
... edge(c, d).
... edge(d, e).
... ''')Now that we've got a db instance, we can run some queries over it.The two core operations are Select and Join.
Select selects tuples - both constants and from rules.
Join selects several tuples at once by unifying logic variables.Let's select some tuples first.Select returns a sequence of pairs(tuples, bindings), where tuples are the selected tuples (always one tuple in fact), and bindings is a mapping of LVars to bound constants.>>> easy.select(db, ('edge', 'a', 'b'))
[((('edge', 'a', 'b'),), {})]Cool!
But what if we wanted to find all edges from a?
This is where logic variables come in.
Logic variables are written as capitalized words in textual datalog, and the easy package recognizes capitalized strings as logic variables when processing queries.There's only one such tuple,edge(a, b), but lets see if we find it.>>> easy.select(db, ('edge', 'a', 'B'))
[((('edge', 'a', 'b'),), {'B': 'b'})]Nice.
But what of joins?
Rules are really a way to give a name to the result of a join, but we can do joins directly too.
For instance, we could try to select all contiguous 2-segment paths.Unlike select which takes a single tuple, join takes a sequence of tuples to simultaneously satisfy.
However select like join returns a sequence of pairs(tuples, bindings), where tuples may actually have many elements.In this case, we're selecting pairs of adjacent edges, so we'll get two tuples and three bindings back in each result.>>> easy.join(db, [
...   ('edge', 'A', 'B'),  # Any edge, ending at B
...   ('edge', 'B', 'C')   # Any edge, beginning at the same B
... ])
[((('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', 'e')),
  {'A': 'c', 'B': 'd', 'C': 'e'})]APIdatalog.typesThe types package provides the core representation used by the rest of the system.
It defines theConstant(value)andLVar(name)tuples types.A datalog tuplea(b, c)is internally represented as(Constant('a'), Constant('b'), Constant('c')').
Tuples containing logic variables simply containLVarinstances in addition toConstantvalues.TheLTupletype alias is for tuples which contain both constants and logic variables.TheCTupletype alias is for tuples containing only constants.ARule(pattern, clauses)is a pair of anLTuplebeing the pattern for the tuples produced by the rule, and a sequence of clauses beingLTuplevalues representing join constraints on the result of the rule.The types package also defines theDatasetclass.
ADatasetis a container for a sequence of tuples, and a sequence of rules which define tuples.
In fact theDatasetclass only has three methodsrules(),tuples()andmerge(other).The query planners work mostly in terms ofDatasetinstances, although extensions ofDatasetmay be better supported.CachedDatasetis an extension of theDatasettype which allows the query engine to cache the result(s) of evaluating rules.
This enables recursive rule evaluation, and some other optimizations.IndexedDatasetis an extension ofCachedDatasetwhich also features support for indices which can reduce the amount of data processed.datalog.parserThis package contains only generated code, and is used to implement the reader.
Its contents are unstable.datalog.readerThe reader only intentionally exposes three methods -readakaread_datasetwhich accepts a string and an optional kwargdb_clsbeing a class extendingDatasetinto which tuples and rules should be read.It also exposesread_commandwhich returns a pair(op: str, val: Either[Rule, LTuple]).
This function is used to implement parts of the REPL, packaged separately (PyPi,git).datalog.evaluatorAt present, the evaluator only contains two methods -selectandjoin.
Select and join are mutually recursive, because rule evaluation is recursively selecting the results of joins.At present, there is only one implementation of select and join in the system.
In the future, this interface will be replaced to add support for query planners.Users should prefer the generally stabledatalog.easyinterface to working directly with the evaluator.datalog.easyA shim over the reader and evaluator designed to make interacting with the evaluator from python more convenient.
Not simpler, just more convenient.read(str, db_cls=IndexedDataset)is just a shim todatalog.reader.readwith a better default class.select(db: Dataset, query: LTuple)eagerly evaluates all results instead of producing a generator, eliminatingConstant()andLVar()wrappers in both tuples and bindings.join(db: Dataset, query: Sequence[LTuple])likewise eagerly evaluates all results, and likewise simplifies results.Usage$ pip install --user arrdem.datalogLimitationsRecursion may have some completeness bugs. I have not yet encountered any, but I
also don't have a strong proof of correctness for the recursive evaluation of
rules yet.The current implementation of negated clauses CANNOT propagate positive
information. This means that negated clauses can only be used in conjunction
with positive clauses. It's not clear if this is an essential limitation.There is as of yet no query planner - not even segmenting rules and tuples by
relation to restrict evaluation. This means that the complexity of a query isO(dataset * term count), which is clearly less than ideal.LicenseMirrored fromhttps://git.arrdem.com/arrdem/datalog-pyPublished under the MIT license. SeeLICENSE.md