Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to construct a query that matches exactly a vector of refs in DataScript?

Setup Consider the following DataScript database of films and cast, with data stolen from learndatalogtoday.org: the following code can be executed in a JVM/Clojure REPL or a ClojureScript REPL, as long as project.clj contains [datascript "0.15.0"] as a dependency.

(ns user
  (:require [datascript.core :as d]))

(def data 
  [["First Blood" ["Sylvester Stallone" "Brian Dennehy" "Richard Crenna"]]
   ["Terminator 2: Judgment Day" ["Linda Hamilton" "Arnold Schwarzenegger" "Edward Furlong" "Robert Patrick"]]
   ["The Terminator" ["Arnold Schwarzenegger" "Linda Hamilton" "Michael Biehn"]]
   ["Rambo III" ["Richard Crenna" "Sylvester Stallone" "Marc de Jonge"]]
   ["Predator 2" ["Gary Busey" "Danny Glover" "Ruben Blades"]]
   ["Lethal Weapon" ["Gary Busey" "Mel Gibson" "Danny Glover"]]
   ["Lethal Weapon 2" ["Mel Gibson" "Joe Pesci" "Danny Glover"]]
   ["Lethal Weapon 3" ["Joe Pesci" "Danny Glover" "Mel Gibson"]]
   ["Alien" ["Tom Skerritt" "Veronica Cartwright" "Sigourney Weaver"]]
   ["Aliens" ["Carrie Henn" "Sigourney Weaver" "Michael Biehn"]]
   ["Die Hard" ["Alan Rickman" "Bruce Willis" "Alexander Godunov"]]
   ["Rambo: First Blood Part II" ["Richard Crenna" "Sylvester Stallone" "Charles Napier"]]
   ["Commando" ["Arnold Schwarzenegger" "Alyssa Milano" "Rae Dawn Chong"]]
   ["Mad Max 2" ["Bruce Spence" "Mel Gibson" "Michael Preston"]]
   ["Mad Max" ["Joanne Samuel" "Steve Bisley" "Mel Gibson"]]
   ["RoboCop" ["Nancy Allen" "Peter Weller" "Ronny Cox"]]
   ["Braveheart" ["Sophie Marceau" "Mel Gibson"]]
   ["Mad Max Beyond Thunderdome" ["Mel Gibson" "Tina Turner"]]
   ["Predator" ["Carl Weathers" "Elpidia Carrillo" "Arnold Schwarzenegger"]]
   ["Terminator 3: Rise of the Machines" ["Nick Stahl" "Arnold Schwarzenegger" "Claire Danes"]]])

(def conn (d/create-conn {:film/cast {:db/valueType :db.type/ref
                                      :db/cardinality :db.cardinality/many}
                          :film/name {:db/unique :db.unique/identity
                                      :db/cardinality :db.cardinality/one}
                          :actor/name {:db/unique :db.unique/identity
                                       :db/cardinality :db.cardinality/one}}))
(def all-datoms (mapcat (fn [[film actors]]
                          (into [{:film/name film}] 
                                (map #(hash-map :actor/name %) actors)))
                        data))
(def all-relations (mapv (fn [[film actors]]
                           {:db/id [:film/name film]
                            :film/cast (mapv #(vector :actor/name %) actors)}) data))

(d/transact! conn all-datoms)
(d/transact! conn all-relations)

Description In a nutshell, there are two kinds of entities in this database—films and actors (word intended to be ungendered)—and three kinds of datoms:

  • film entity: :film/name (a unique string)
  • film entity: :film/cast (multiple refs)
  • actor entity: :actor/name (unique string)

Question I would like to construct a query which asks: which films have these N actors, and these N actors alone, appeared as the sole stars, for N>=2?

E.g., RoboCop starred Nancy Allen, Peter Weller, Ronny Cox, but no film starred solely the first two of these, Allen and Weller. Therefore, I would expect the following query to produce the empty set:

(d/q '[:find ?film-name
       :where
       [?film :film/name ?film-name]
       [?film :film/cast ?actor-1]
       [?film :film/cast ?actor-2]
       [?actor-1 :actor/name "Nancy Allen"]
       [?actor-2 :actor/name "Peter Weller"]]
     @conn)
; => #{["RoboCop"]}

However, the query is flawed because I don't know how to express that any matches should exclude any actors who are not Allen or Weller—again, I want to find the movies where only Allen and Weller have collaborated without any other actors, so I want to adapt the above query to produce the empty set. How can I adjust this query to enforce this requirement?

like image 942
Ahmed Fasih Avatar asked May 13 '16 02:05

Ahmed Fasih


1 Answers

Because DataScript doesn't have negation (as of May 2016), I don't believe that's possible with one static query in 'pure' Datalog.

My way to go would be:

  1. build the query programmatically to add the N clauses that state that the cast must contain the N actors
  2. Add a predicate function which, given a movie, the database, and the set of actors ids, uses the EAVT index to find if each movie has an actor that is not in the set.

Here's a basic implementation

(defn only-those-actors? [db movie actors]
  (->> (datoms db :eavt movie :film/cast) seq
    (every? (fn [[_ _ actor]]
                (contains? actors actor)))
    ))

(defn find-movies-with-exact-cast [db actors-names]
  (let [actors (set (d/q '[:find [?actor ...] :in $ [?name ...] ?only-those-actors :where
                           [?actor :actor/name ?name]]
                      db actors-names))
        query {:find '[[?movie ...]]
               :in '[$ ?actors ?db]
               :where
               (concat
                 (for [actor actors]
                   ['?movie :film/cast actor])
                 [['(only-those-actors? ?db ?movie ?actors)]])}]
    (d/q query db actors db only-those-actors?)))
like image 120
Valentin Waeselynck Avatar answered Oct 22 '22 19:10

Valentin Waeselynck