Class: Diametric::Query

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/diametric/query.rb

Overview

Query objects are used to generate Datomic queries, whether to send via an external client or via the persistence API. The two methods used to generate a query are .where and .filter, both of which are chainable. To get the query data and arguments for a Query, use the data method.

If you are using a persistence API, you can ask Query to get the results of a Datomic query. Diametric::Query is an Enumerable. To get the results of a query, use Enumerable methods such as .each or .first. Query also provides a .all method to run the query and get the results.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model, connection_or_database = nil, resolve = false) ⇒ Query

Create a new Datomic query.


25
26
27
28
29
30
31
32
33
# File 'lib/diametric/query.rb', line 25

def initialize(model, connection_or_database=nil, resolve=false)
  @model = model
  @conditions = {}
  @filters = []
  @filter_attrs = []
  @filter_values = []
  @conn_or_db = connection_or_database
  @resolve = resolve
end

Instance Attribute Details

#conditionsObject

Returns the value of attribute conditions


19
20
21
# File 'lib/diametric/query.rb', line 19

def conditions
  @conditions
end

#connectionObject (readonly)

Returns the value of attribute connection


19
20
21
# File 'lib/diametric/query.rb', line 19

def connection
  @connection
end

#filter_attrsObject

Returns the value of attribute filter_attrs


19
20
21
# File 'lib/diametric/query.rb', line 19

def filter_attrs
  @filter_attrs
end

#filter_valuesObject

Returns the value of attribute filter_values


19
20
21
# File 'lib/diametric/query.rb', line 19

def filter_values
  @filter_values
end

#filtersObject

Returns the value of attribute filters


19
20
21
# File 'lib/diametric/query.rb', line 19

def filters
  @filters
end

#modelObject (readonly)

Returns the value of attribute model


19
20
21
# File 'lib/diametric/query.rb', line 19

def model
  @model
end

#resolveObject (readonly)

Returns the value of attribute resolve


19
20
21
# File 'lib/diametric/query.rb', line 19

def resolve
  @resolve
end

Instance Method Details

#all(conn_or_db = @conn_or_db) ⇒ Array<Entity>

Return all query results.


136
137
138
139
140
141
142
# File 'lib/diametric/query.rb', line 136

def all(conn_or_db=@conn_or_db)
  if self.model.instance_variable_get("@peer") && !@resolve
    model.q(*data, conn_or_db)
  else
    map { |x| x }
  end
end

#dataArray(Array, Array)

Create a Datomic query from the conditions and filters passed to this Query object.


150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/diametric/query.rb', line 150

def data
  return peer_data if self.model.instance_variable_get("@peer")

  vars = model.attribute_names.map { |attribute| ~"?#{attribute}" }

  from = conditions.map { |k, _| ~"?#{k}" }

  clauses = model.attribute_names.map { |attribute|
    [~"?e", model.namespace(model.prefix, attribute), ~"?#{attribute}"]
  }
  clauses += filters

  args = conditions.map { |_, v| v }

  query = [
    :find, ~"?e", *vars,
    :in, ~"\$", *from,
    :where, *clauses
  ]

  [query, args]
end

#each {|Entity| ... } ⇒ Object

Loop through the query results. In order to use each, your model must include a persistence API. At a minimum, it must have a .q method that returns an Enumerable object.

Yields:

  • (Entity)

    An instance of the model passed to Query.


115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/diametric/query.rb', line 115

def each
  # TODO check to see if the model has a `.q` method and give
  # an appropriate error if not.
  res = model.q(*data, @conn_or_db)
  collapse_results(res).each do |entity|
    if @resolve
      yield model.reify(entity.first, @conn_or_db, @resolve)
    elsif self.model.instance_variable_get("@peer")
      yield entity
    # The map is for compatibility with Java peer persistence.
    # TODO remove if possible
    else
      yield model.from_query(entity.map { |x| x }, @conn_or_db, @resolve)
    end
  end
end

#filter(*filter) ⇒ Query

Add a filter to your Datomic query. Filters are known as expression clause predicates in the Datomic query documentation.

A filter can be in one of two forms. In the first, you pass a series of arguments. Any Ruby symbol given in this form will be converted to a EDN symbol. If the symbol is the same as one of the queried model's attributes or as a key passed to where, it will be prefixed with a ? so that it becomes a Datalog variable. In the second form, you pass EDN representing a Datomic predicate to filter. No conversion is done on this filter and it must be an EDN list.

Examples:

Passing arguments to be converted.

query.filter(:>, :age, 21)                # REST
query.filter(connection, :>, :age, 21)    # Peer

Passing EDN, which will not be converted.

query.filter(EDN::Type::List.new(EDN::Type::Symbol(">"),
                                 EDN::Type::Symbol("?age"),
                                 21))
# or, more simply
query.filter(~[~">", ~"?age", 21])

77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/diametric/query.rb', line 77

def filter(*filter)
  return peer_filter(*filter) if self.model.instance_variable_get("@peer")
  query = self.dup

  if filter.first.is_a?(EDN::Type::List)
    filter = filter.first
  else
    filter = filter.map { |e| convert_filter_element(e) }
    filter = EDN::Type::List.new(*filter)
  end
  query.filters += [[filter]]
  query
end

#peer_dataObject


173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/diametric/query.rb', line 173

def peer_data
  if conditions.empty? && filters.empty?
    args = [model.prefix]
    query = <<-EOQ
[:find ?e
 :in $ [?include-ns ...]
 :where
 [?e ?aid ?v]
 [?aid :db/ident ?a]
 [(namespace ?a) ?ns]
 [(= ?ns ?include-ns)]]
EOQ
  else
    from = conditions.map do |k, v|
      if v.kind_of? Array
        [~"?#{k}",
         Diametric::Persistence::Utils.read_string("...")]
      else
        ~"?#{k}"
      end
    end

    keys = conditions.keys
    unless filter_attrs.empty?
      from += filter_attrs.inject([]) { |memo, key| memo << ~"?#{key}value"; memo }
      keys += filter_attrs
    end
    keys.uniq!

    clauses = keys.map { |attribute|
      [~"?e", model.namespace(model.prefix, attribute), ~"?#{attribute}"]
    }
    clauses += filters

    args = conditions.map { |_, v| v }
    args += filter_values

    query = [
             :find, ~"?e",
             :in, ~"\$", from.flatten(1),
             :where, *clauses
            ]
  end
  [query, args]
end

#peer_filter(*filter) ⇒ Object


91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/diametric/query.rb', line 91

def peer_filter(*filter)
  query = self.dup
  query.filter_attrs += (self.model.attribute_names & filter)
  filter = filter.map do |e|
    if e.is_a? Symbol
      convert_filter_element(e)
    elsif e.is_a? String
      e
    else
      query.filter_values << e
      ~"?#{query.filter_attrs.last.to_s}value"
    end
  end
  filter = EDN::Type::List.new(*filter)
  query.filters << [Diametric::Persistence::Utils.read_string(filter.to_edn)]
  query
end

#where(conditions) ⇒ Query

Add conditions to your Datomic query. Conditions check for equality against entity attributes. In addition, you can add conditions for use as variables in filters.

Examples:

Looking for mice named Wilbur.

Query.new(Mouse).conditions(:name => "Wilbur")

44
45
46
47
48
# File 'lib/diametric/query.rb', line 44

def where(conditions)
  query = self.dup
  query.conditions = query.conditions.merge(conditions)
  query
end