Class: Mao::Query
- Inherits:
-
Object
- Object
- Mao::Query
- Defined in:
- lib/mao/query.rb
Overview
A persistent query structure which can be manipulated by creating new persistent queries, and eventually executed.
All “state” about the query itself is deliberately stored in a simple Hash, @options, to provide transparency and ensure simplicity of the resulting design.
Defined Under Namespace
Classes: JoinContext, Raw
Instance Attribute Summary collapse
-
#col_types ⇒ Object
readonly
The cached information about columns and their types for the table being referred to.
-
#options ⇒ Object
readonly
The immutable options hash of this Query instance.
-
#table ⇒ Object
readonly
A symbol of the name of the table this Query points to.
Class Method Summary collapse
-
.raw(text) ⇒ Object
Returns a Mao::Query::Raw with
text.
Instance Method Summary collapse
-
#initialize(table, options = {}, col_types = nil) ⇒ Query
constructor
Constructs the Query with reference to a table named
table, and immutable options hashoptions. -
#insert!(*rows) ⇒ Object
Inserts
rowsinto the table. -
#join(target, &block) ⇒ Object
Joins the results of this table against another table,
target. -
#limit(n) ⇒ Object
Restricts the query to at most
nresults. -
#only(*columns) ⇒ Object
Only returns the given
columns, Symbols (possibly nested in Arrays). -
#order(column, direction) ⇒ Object
Returns the query’s results sorted by
columnindirection, either :asc or :desc. -
#returning(*columns) ⇒ Object
For INSERTs, returns
columnsfor inserted rows. -
#select! ⇒ Object
Executes the constructed query and returns an Array of Hashes of results.
-
#select_first! ⇒ Object
Limits the query to one result, and returns that result.
-
#sql ⇒ Object
Constructs the SQL for this query.
-
#update!(changes) ⇒ Object
Executes the changes in Hash
changesto the rows matching this object, returning the number of affected rows. -
#where(&block) ⇒ Object
Filters results based on the conditions specified in
block. -
#with_options(options) ⇒ Object
Returns a new Mao::Query with
optionsmerged into the options of this object.
Constructor Details
#initialize(table, options = {}, col_types = nil) ⇒ Query
Constructs the Query with reference to a table named table, and immutable options hash options. col_types is column information for the table, usually populated by a prior invocation of Mao::Query.new.
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/mao/query.rb', line 29 def initialize(table, ={}, col_types=nil) @table, @options = table.to_sym, .freeze if !col_types col_types = {} Mao.sql( 'SELECT column_name, data_type FROM information_schema.columns ' \ 'WHERE table_name=$1', [@table.to_s]) do |pg_result| if pg_result.num_tuples.zero? raise ArgumentError, "invalid or blank table #@table" end pg_result.each do |tuple| col_types[tuple["column_name"].to_sym] = tuple["data_type"] end end end @col_types = col_types.freeze end |
Instance Attribute Details
#col_types ⇒ Object (readonly)
The cached information about columns and their types for the table being referred to.
59 60 61 |
# File 'lib/mao/query.rb', line 59 def col_types @col_types end |
#options ⇒ Object (readonly)
The immutable options hash of this Query instance.
55 56 57 |
# File 'lib/mao/query.rb', line 55 def @options end |
#table ⇒ Object (readonly)
A symbol of the name of the table this Query points to.
52 53 54 |
# File 'lib/mao/query.rb', line 52 def table @table end |
Class Method Details
Instance Method Details
#insert!(*rows) ⇒ Object
Inserts rows into the table. No other options should be applied to this query. Returns the number of inserted rows, unless #returning was called, in which case the calculated values from the INSERT are returned.
370 371 372 373 374 375 376 377 378 |
# File 'lib/mao/query.rb', line 370 def insert!(*rows) Mao.sql((:insert => rows.flatten).sql) do |pg_result| if @options[:returning] pg_result.map {|result| Mao.normalize_result(result, @col_types)} else pg_result.cmd_tuples end end end |
#join(target, &block) ⇒ Object
Joins the results of this table against another table, target. The conditions for joining one row in this table against one in target are specified in block.
block is per #where’s, except the names in context are tables, not columns; the tables returned are the same as the context of #where itself, so for instance, “blah.x == 3” will filter where the column “x” of table “blah” (which should be either this table, or the target table) equals 3.
Boolean operations are then all per #where.
If block is not specified, target is instead treated as a Hash of the form => {local_key => foreign_key}.
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/mao/query.rb', line 202 def join(target, &block) if !block local_table = @table foreign_table = target.keys[0] mapping = target[foreign_table] local_key = mapping.keys[0] foreign_key = mapping[local_key] return join(foreign_table) { send(local_table).send(local_key) == send(foreign_table).send(foreign_key) } end context = JoinContext.new.freeze (:join => [target, context.instance_exec(&block).finalize]) end |
#limit(n) ⇒ Object
Restricts the query to at most n results.
68 69 70 71 72 73 74 |
# File 'lib/mao/query.rb', line 68 def limit(n) unless n.is_a? Integer raise ArgumentError, "#{n.inspect} not an Integer" end (:limit => n.to_i) end |
#only(*columns) ⇒ Object
Only returns the given columns, Symbols (possibly nested in Arrays).
If columns is a single argument, and it’s a Hash, the keys should be Symbols corresponding to table names, and the values Arrays of Symbol column names. This is only for use with #join, and #only must be called after #join.
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/mao/query.rb', line 97 def only(*columns) columns = columns.flatten if columns.length == 1 and columns[0].is_a?(Hash) unless @options[:join] raise ArgumentError, "#only with a Hash must be used only after #join" end other = Mao.query(@options[:join][0]) columns = columns[0] columns.each do |table, table_columns| unless table_columns.is_a? Array raise ArgumentError, "#{table_columns.inspect} is not an Array" end if table == @table table_columns.each do |column| check_column(column, @table, @col_types) end elsif table == other.table table_columns.each do |column| check_column(column, other.table, other.col_types) end else raise ArgumentError, "#{table} is not a column in this query" end end else columns.each do |column| check_column(column, @table, @col_types) end end (:only => columns) end |
#order(column, direction) ⇒ Object
Returns the query’s results sorted by column in direction, either :asc or :desc.
78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/mao/query.rb', line 78 def order(column, direction) unless column.is_a?(Symbol) and [:asc, :desc].include?(direction) raise ArgumentError, "#{column.inspect} not a Symbol or " \ "#{direction.inspect} not :asc or :desc" end check_column(column, @table, @col_types) direction = direction == :asc ? "ASC" : "DESC" (:order => [column, direction]) end |
#returning(*columns) ⇒ Object
For INSERTs, returns columns for inserted rows.
134 135 136 137 138 139 140 141 142 |
# File 'lib/mao/query.rb', line 134 def returning(*columns) columns = columns.flatten columns.each do |column| check_column(column, @table, @col_types) end (:returning => columns) end |
#select! ⇒ Object
Executes the constructed query and returns an Array of Hashes of results.
340 341 342 343 344 345 346 347 348 349 350 351 352 |
# File 'lib/mao/query.rb', line 340 def select! # Ensure we can never be destructive by nilifying :update. Mao.sql((:update => nil).sql) do |pg_result| if @options[:join] other = Mao.query(@options[:join][0]) pg_result.map {|result| Mao.normalize_join_result(result, self, other) } else pg_result.map {|result| Mao.normalize_result(result, @col_types)} end end end |
#select_first! ⇒ Object
Limits the query to one result, and returns that result.
355 356 357 |
# File 'lib/mao/query.rb', line 355 def select_first! limit(1).select!.first end |
#sql ⇒ Object
Constructs the SQL for this query.
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 |
# File 'lib/mao/query.rb', line 221 def sql s = "" = @options.dup if update = .delete(:update) s = "UPDATE " s << Mao.quote_ident(@table) s << " SET " if update.length == 0 raise ArgumentError, "invalid update: nothing to set" end s << update.map do |column, value| check_column(column, @table, @col_types) "#{Mao.quote_ident(column)} = #{Mao.escape_literal(value)}" end.join(", ") if where = .delete(:where) s << " WHERE " s << Mao::Filter.sql(where) end elsif insert = .delete(:insert) s = "INSERT INTO " s << Mao.quote_ident(@table) s << " (" keys = insert.map(&:keys).flatten.uniq.sort s << keys.map do |column| check_column(column, @table, @col_types) Mao.quote_ident(column) end.join(", ") s << ") VALUES " first = true insert.each do |row| if first first = false else s << ", " end s << "(" s << keys.map {|k| if row.include?(k) Mao.escape_literal(row[k]) else "DEFAULT" end }.join(", ") s << ")" end if returning = .delete(:returning) s << " RETURNING " s << returning.map {|c| Mao.quote_ident(c)}.join(", ") end else s = "SELECT " join = .delete(:join) only = .delete(:only) if join n = 0 s << (@col_types.keys.sort.map {|c| n += 1 if !only or (only[@table] and only[@table].include?(c)) "#{Mao.quote_ident(@table)}.#{Mao.quote_ident(c)} " + "#{Mao.quote_ident("c#{n}")}" end } + Mao.query(join[0]).col_types.keys.sort.map {|c| n += 1 if !only or (only[join[0]] and only[join[0]].include?(c)) "#{Mao.quote_ident(join[0])}.#{Mao.quote_ident(c)} " + "#{Mao.quote_ident("c#{n}")}" end }).reject(&:nil?).join(", ") elsif only s << only.map {|c| Mao.quote_ident(c)}.join(", ") else s << "*" end s << " FROM #{Mao.quote_ident(@table)}" if join s << " INNER JOIN #{Mao.quote_ident(join[0])} ON " s << Mao::Filter.sql(join[1]) end if where = .delete(:where) s << " WHERE " s << Mao::Filter.sql(where) end if order = .delete(:order) s << " ORDER BY " s << Mao.quote_ident(order[0]) s << " " s << order[1] end if limit = .delete(:limit) s << " LIMIT #{limit}" end end if .length > 0 raise ArgumentError, "invalid options in #sql: #{.inspect}. " \ "SQL constructed: #{s}" end s end |
#update!(changes) ⇒ Object
Executes the changes in Hash changes to the rows matching this object, returning the number of affected rows.
361 362 363 364 365 |
# File 'lib/mao/query.rb', line 361 def update!(changes) Mao.sql((:update => changes).sql) do |pg_result| pg_result.cmd_tuples end end |
#where(&block) ⇒ Object
Filters results based on the conditions specified in block.
Depending on if #join has been called, one of two things occur:
1. If #join has not been called, +block+ has available in context the
column names of the table being queried; or,
2. If #join has been called, +block+ has available in context the table
names of the tables involved in the query. Per #join, those objects
will have the columns available as methods.
Once you have a column, use regular operators to construct tests, e.g. “x
3“ will filter where the column ”x“ has value 3.
Boolean operations on columns return Mao::Filter objects; use #and and #or to combine them. The return value of the block should be the full desired filter.
179 180 181 182 183 184 185 186 187 |
# File 'lib/mao/query.rb', line 179 def where(&block) if @options[:join] context = JoinContext.new.freeze else context = Mao::Filter::Table.new(self, false).freeze end (:where => context.instance_exec(&block).finalize) end |
#with_options(options) ⇒ Object
Returns a new Mao::Query with options merged into the options of this object.
63 64 65 |
# File 'lib/mao/query.rb', line 63 def () self.class.new(@table, @options.merge(), @col_types).freeze end |