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 read_rule_id ),
  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 }.freeze
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' }.freeze

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, #restrict_reference, #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, comment = nil) ⇒ Query

Returns a new instance of Query.



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

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

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

  @comment = comment || default_comment

  interpret @statement
  self
end

Instance Attribute Details

#commentObject (readonly)

Returns the value of attribute comment.



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

def comment
  @comment
end

#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, comment = nil) ⇒ 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)



106
107
108
# File 'lib/card/query.rb', line 106

def self.run statement, comment=nil
  new(statement, comment).run
end

Instance Method Details

#add_condition(*args) ⇒ Object



236
237
238
239
240
241
242
243
# File 'lib/card/query.rb', line 236

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

#all_joinsObject



286
287
288
289
# File 'lib/card/query.rb', line 286

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

#clause_to_hash(clause) ⇒ Object



182
183
184
185
186
187
188
189
# File 'lib/card/query.rb', line 182

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 raise BadQuery, "Invalid query args #{clause.inspect}"
  end
end

#contextObject



211
212
213
214
215
216
217
# File 'lib/card/query.rb', line 211

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

#current_conjunctionObject



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

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

#default_commentObject



96
97
98
99
# File 'lib/card/query.rb', line 96

def default_comment
  return if @superquery || !Card.config.sql_comments
  statement.to_s
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



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/card/query.rb', line 124

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



169
170
171
# File 'lib/card/query.rb', line 169

def interpret clause
  interpret_by_key normalize_clause(clause)
end

#interpret_attributes(key, val) ⇒ Object



245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/card/query.rb', line 245

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                   raise BadQuery, "Invalid attribute #{key}"
  end
end

#interpret_by_key(clause) ⇒ Object



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/card/query.rb', line 219

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



173
174
175
176
177
178
179
180
# File 'lib/card/query.rb', line 173

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



200
201
202
203
204
205
206
207
208
209
# File 'lib/card/query.rb', line 200

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



191
192
193
194
195
196
197
198
# File 'lib/card/query.rb', line 191

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

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



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/card/query.rb', line 265

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



258
259
260
261
262
263
# File 'lib/card/query.rb', line 258

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.…



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

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

#runObject

run the current query

Returns:

  • array of card objects by default



112
113
114
115
116
117
118
119
120
121
# File 'lib/card/query.rb', line 112

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



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

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

#sqlObject



146
147
148
# File 'lib/card/query.rb', line 146

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

#subquery(opts = {}) ⇒ Object



159
160
161
162
163
# File 'lib/card/query.rb', line 159

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