Class: Card::Query

Inherits:
Object show all
Includes:
Attributes, Clause
Defined in:
lib/card/query.rb,
lib/card/query/join.rb,
lib/card/query/value.rb,
lib/card/query/reference.rb,
lib/card/query/attributes.rb,
lib/card/query/sql_statement.rb

Overview

Card::Query is for finding implicit lists (or counts of lists) of cards.

Search and Set cards use Card::Query to query the database, and it’s also frequently used directly in code.

Query “statements” (objects, really) are made in WQL (Wagn Query Language). Because WQL is used by Wagneers, the primary language documentation is on wagn.org. (wagn.org/WQL_Syntax). Note that the examples there are in JSON, like Search card content, but statements in Card::Query are in ruby form.

In Wagn’s current form, Card::Query generates and executes SQL statements. However, the SQL generation is largely (not yet fully) separated from the WQL statement interpretation.

The most common way to use Card::Query is as follows:

list_of_cards = Card::Query.run(statement)

This is equivalent to:

query = Card::Query.new(statement)
list_of_cards = query.run

Upon initiation, the query is interpreted, and the following key objects are populated:

  • @join - an Array of Card::Query::Join objects

  • @conditions - an Array of conditions

  • @mod - a Hash of other query-altering keys

  • @subqueries - a list of other queries nested within this one

Each condition is either a SQL-ready string (boo) or an Array in this form:

[ field_string_or_sym, Card::Value::Query object ]

Defined Under Namespace

Modules: Attributes, Clause Classes: Join, Reference, SqlStatement, Value

Constant Summary collapse

ATTRIBUTES =
{
  basic:           %w( id name key type_id content left_id right_id
                       creator_id updater_id codename                     ),
  relational:      %w( type part left right
                       editor_of edited_by last_editor_of last_edited_by
                       creator_of created_by member_of member             ),
  plus_relational: %w( plus left_plus right_plus                          ),
  ref_relational:  %w( refer_to referred_to_by
                       link_to linked_to_by
                       include included_by                                ),
  conjunction:     %w( and or all any                                     ),
  special:         %w( found_by not sort match complete extension_type    ),
  ignore:          %w( prepend append view params vars size )
}.inject({}) {|h,pair| pair[1].each { |v| h[v.to_sym]=pair[0] }; h }
CONJUNCTIONS =
{ any: :or, in: :or, or: :or, all: :and, and: :and }
MODIFIERS =
%w( conj return sort sort_as group dir limit offset )
.inject({}) { |h,v| h[v.to_sym]=nil; h }
OPERATORS =
%w( != = =~ < > in ~ ).inject({}) {|h,v| h[v]=v; h }.merge({
  eq: '=', gt: '>', lt: '<', match: '~', ne: '!=', :'not in'=> nil
}.stringify_keys)
DEFAULT_ORDER_DIRS =
{ :update => "desc", :relevance => "desc" }

Constants included from Attributes

Attributes::SORT_JOIN_TO_ITEM_MAP

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Attributes

#all, #any, #complete, #conjoin, #conjunction, #created_by, #creator_of, #edited_by, #editor_of, #extension_type, #found_by, #found_by_cards, #id_from_val, #join_cards, #join_references, #junction, #last_edited_by, #last_editor_of, #left, #left_plus, #match, #member, #member_of, #not, #part, #plus, #restrict, #right, #right_plus, #sort, #sort_by_count, #table_alias, #table_id, #tick_table_seq!, #type

Methods included from Clause

#match_prep, #quote, #safe_sql

Constructor Details

#initialize(statement) ⇒ Query

Returns a new instance of Query.



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

def initialize statement
  @subqueries = []
  @conditions = []
  @joins = []
  @mods = {}
  @statement = statement.clone

  @unjoined   = @statement.delete(:unjoined  ) || nil
  @context    = @statement.delete(:context   ) || nil
  @superquery = @statement.delete(:superquery) || nil
  @vars       = @statement.delete(:vars      ) || {}
  @vars.symbolize_keys!

  interpret @statement
  self
end

Instance Attribute Details

#conditionsObject (readonly)

Returns the value of attribute conditions.



73
74
75
# File 'lib/card/query.rb', line 73

def conditions
  @conditions
end

#conditions_on_joinObject

Returns the value of attribute conditions_on_join.



75
76
77
# File 'lib/card/query.rb', line 75

def conditions_on_join
  @conditions_on_join
end

#joinsObject

Returns the value of attribute joins.



75
76
77
# File 'lib/card/query.rb', line 75

def joins
  @joins
end

#modsObject (readonly)

Returns the value of attribute mods.



73
74
75
# File 'lib/card/query.rb', line 73

def mods
  @mods
end

#statementObject (readonly)

Returns the value of attribute statement.



73
74
75
# File 'lib/card/query.rb', line 73

def statement
  @statement
end

#subqueriesObject (readonly)

Returns the value of attribute subqueries.



73
74
75
# File 'lib/card/query.rb', line 73

def subqueries
  @subqueries
end

#superqueryObject (readonly)

Returns the value of attribute superquery.



73
74
75
# File 'lib/card/query.rb', line 73

def superquery
  @superquery
end

#table_seqObject

Returns the value of attribute table_seq.



75
76
77
# File 'lib/card/query.rb', line 75

def table_seq
  @table_seq
end

#unjoinedObject

Returns the value of attribute unjoined.



75
76
77
# File 'lib/card/query.rb', line 75

def unjoined
  @unjoined
end

Class Method Details

.run(statement) ⇒ Object

Query Execution By default a query returns card objects. This is accomplished by returning a card identifier from SQL and then hooking into our caching system (see Card::Fetch)



99
100
101
102
# File 'lib/card/query.rb', line 99

def self.run statement
  query = new statement
  query.run
end

Instance Method Details

#add_condition(*args) ⇒ Object



230
231
232
233
234
235
236
237
# File 'lib/card/query.rb', line 230

def add_condition *args
  @conditions <<
    if args.size > 1
      [args.shift, Value.new(args.shift, self)]
    else
      args[0]
    end
end

#all_joinsObject



280
281
282
283
# File 'lib/card/query.rb', line 280

def all_joins
  @all_joins ||=
    (joins + subqueries.find_all(&:unjoined).map(&:all_joins)).flatten
end

#clause_to_hash(clause) ⇒ Object



176
177
178
179
180
181
182
183
# File 'lib/card/query.rb', line 176

def clause_to_hash clause
  case clause
  when Hash    then clause
  when String  then { key: clause.to_name.key }
  when Integer then { id: clause }
  else fail BadQuery, "Invalid query args #{clause.inspect}"
  end
end

#contextObject



205
206
207
208
209
210
211
# File 'lib/card/query.rb', line 205

def context
  if !@context.nil?
    @context
  else
    @context = @superquery ? @superquery.context : ''
  end
end

#current_conjunctionObject



276
277
278
# File 'lib/card/query.rb', line 276

def current_conjunction
  @mods[:conj].blank? ? :and : @mods[:conj]
end

#get_results(retrn) ⇒ Object

Returns Integer for :count, otherwise Array of Strings or Integers.

Returns:

  • Integer for :count, otherwise Array of Strings or Integers



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/card/query.rb', line 118

def get_results retrn
  rows = run_sql
  if retrn == 'name' && (statement[:prepend] || statement[:append])
    rows.map do |row|
      [statement[:prepend], row['name'], statement[:append]].compact * '+'
    end
  else
    case retrn
    when 'count' then rows.first['count'].to_i
    when 'raw'   then rows
    when /id$/   then rows.map { |row| row[retrn].to_i }
    else              rows.map { |row| row[retrn]      }
    end
  end
end

#interpret(clause) ⇒ Object

normalize and extract meaning from a clause

Parameters:

  • clause (Hash, String, Integer)

    statement or chunk thereof



163
164
165
# File 'lib/card/query.rb', line 163

def interpret clause
  interpret_by_key normalize_clause(clause)
end

#interpret_attributes(key, val) ⇒ Object



239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/card/query.rb', line 239

def interpret_attributes key, val
  case ATTRIBUTES[key]
  when :basic            then add_condition key, val
  when :conjunction      then send key, val
  when :relational       then relate key, val
  when :special          then relate key, val
  when :ref_relational   then relate key, val, method: :join_references
  when :plus_relational  then relate_compound key, val
  when :ignore           then # noop
  else                   fail BadQuery, "Invalid attribute #{key}"
  end
end

#interpret_by_key(clause) ⇒ Object



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/card/query.rb', line 213

def interpret_by_key clause
  clause.each do |key, val|
    case
    when OPERATORS.key?(key.to_s) && !ATTRIBUTES[key]
      # eg "match" is both operator and attribute;
      # interpret as attribute when "match" is key
      interpret content: [key, val]
    when MODIFIERS.key?(key) && !clause[key].is_a?(Hash)
      # eg when "sort" is hash, it can have subqueries
      # and must be interpreted like an attribute
      @mods[key] = val.is_a?(Array) ? val : val.to_s
    else
      interpret_attributes key, val
    end
  end
end

#normalize_clause(clause) ⇒ Object



167
168
169
170
171
172
173
174
# File 'lib/card/query.rb', line 167

def normalize_clause clause
  clause = clause_to_hash clause
  clause.symbolize_keys!
  clause.each do |key, val|
    clause[key] = normalize_value val
  end
  clause
end

#normalize_string_value(val) ⇒ Object



194
195
196
197
198
199
200
201
202
203
# File 'lib/card/query.rb', line 194

def normalize_string_value val
  case val.to_s
  when /^\$(\w+)$/                       # replace from @vars
    @vars[Regexp.last_match[1].to_sym].to_s.strip
  when /\b_/                             # absolutize based on @context
    val.to_name.to_absolute(context)
  else
    val
  end
end

#normalize_value(val) ⇒ Object



185
186
187
188
189
190
191
192
# File 'lib/card/query.rb', line 185

def normalize_value val
  case val
  when Integer, Float, Symbol, Hash then val
  when String, Card::Name           then normalize_string_value val
  when Array                        then val.map { |v| normalize_value v }
  else fail BadQuery, "unknown WQL value type: #{val.class}"
  end
end

#relate(key, val, opts = {}) ⇒ Object



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/card/query.rb', line 259

def relate key, val, opts={}
  multiple = opts[:multiple].nil? ? val.is_a?(Array) : opts[:multiple]
  method = opts[:method] || :send

  if multiple
    conj = conjunction(val.first) ? conjunction(val.shift) : :and
    if conj == current_conjunction
      # same conjunction as container, no need for subcondition
      val.each { |v| send method, key, v }
    else
      send conj, val.map { |v| { key => v } }
    end
  else
    send method, key, val
  end
end

#relate_compound(key, val) ⇒ Object



252
253
254
255
256
257
# File 'lib/card/query.rb', line 252

def relate_compound key, val
  has_multiple_values =
    val.is_a?(Array) &&
    (val.first.is_a?(Array) || conjunction(val.first).present?)
  relate key, val, multiple: has_multiple_values
end

#rootObject

Query Hierarchy @root, @subqueries, and @superquery are used to track a hierarchy of query objects. This nesting allows to find, for example, cards that link to cards that link to cards.…



149
150
151
# File 'lib/card/query.rb', line 149

def root
  @root ||= @superquery ? @superquery.root : self
end

#runObject

run the current query

Returns:

  • array of card objects by default



106
107
108
109
110
111
112
113
114
115
# File 'lib/card/query.rb', line 106

def run
  retrn = statement[:return].present? ? statement[:return].to_s : 'card'
  if retrn == 'card'
    get_results('name').map do |name|
      Card.fetch name, new: {}
    end
  else
    get_results retrn
  end
end

#run_sqlObject



134
135
136
137
138
# File 'lib/card/query.rb', line 134

def run_sql
  # puts "\nstatement = #{@statement}"
  # puts "sql = #{sql}"
  ActiveRecord::Base.connection.select_all(sql)
end

#sqlObject



140
141
142
# File 'lib/card/query.rb', line 140

def sql
  @sql ||= SqlStatement.new(self).build.to_s
end

#subquery(opts = {}) ⇒ Object



153
154
155
156
157
# File 'lib/card/query.rb', line 153

def subquery opts={}
  subquery = Query.new opts.merge(superquery: self)
  @subqueries << subquery
  subquery
end