mao 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.autotest +2 -0
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +18 -0
- data/README.md +14 -0
- data/Rakefile +8 -0
- data/TODO +5 -0
- data/lib/mao/filter.rb +168 -0
- data/lib/mao/query.rb +381 -0
- data/lib/mao/version.rb +5 -0
- data/lib/mao.rb +180 -0
- data/mao.gemspec +22 -0
- data/spec/filter_spec.rb +173 -0
- data/spec/fixture.sql +38 -0
- data/spec/mao_spec.rb +220 -0
- data/spec/query_spec.rb +417 -0
- data/spec/spec_helper.rb +13 -0
- data/thoughts.rb +42 -0
- metadata +120 -0
data/.autotest
ADDED
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
/pkg
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
data/README.md
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# Mao [](http://travis-ci.org/unnali/mao)
|
2
|
+
|
3
|
+
**Mao Ain't an ORM.**
|
4
|
+
|
5
|
+
## mission/tenets
|
6
|
+
|
7
|
+
## authorship
|
8
|
+
|
9
|
+
* [Timothy Leslie Allen](https://github.com/timothyleslieallen).
|
10
|
+
* [Arlen Christian Mart Cuss](https://github.com/unnali).
|
11
|
+
|
12
|
+
## copyright and licensing
|
13
|
+
|
14
|
+
TBD.
|
data/Rakefile
ADDED
data/TODO
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
Specify error handling at all levels.
|
2
|
+
Joins/eager loads. What syntax? How do we prevent unexpected blowouts?
|
3
|
+
- no auto subqueries, etc. Thinner wrapper over SQL than AR.
|
4
|
+
|
5
|
+
Do JOINs without all the "c1" "c2" "c3" business; assume (= check we can assume) that the primary table's columns come first, and secondary table's after; then just pick off the first n cols of a result tuple (where n is number of cols in primary table) and assign them to the primary table, and the remaining m (where m is no. of cols in secondary) to secondary.
|
data/lib/mao/filter.rb
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# A mix-in for any kind of filter in a where clause (vis-à-vis
|
4
|
+
# Mao::Query#where).
|
5
|
+
module Mao::Filter
|
6
|
+
# If +obj+ is a Mao::Filter, call #finalize on it; otherwise, use
|
7
|
+
# Mao.escape_literal to escape +obj.to_s+.
|
8
|
+
def self.finalize_or_literal(obj)
|
9
|
+
if obj.is_a? Mao::Filter
|
10
|
+
obj.finalize
|
11
|
+
else
|
12
|
+
Mao.escape_literal(obj)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Generate the SQL for the finalized object +finalized+. If +finalized+ is a
|
17
|
+
# String, it's returned without modification.
|
18
|
+
def self.sql(finalized)
|
19
|
+
if finalized.is_a? String
|
20
|
+
finalized
|
21
|
+
else
|
22
|
+
klass, *args = finalized
|
23
|
+
Mao::Filter.const_get(klass).sql(*args)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(options={})
|
28
|
+
@options = options.freeze
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_reader :options
|
32
|
+
|
33
|
+
# Returns an AND filter where the current object is the LHS and +rhs+ is the
|
34
|
+
# RHS.
|
35
|
+
def and(rhs)
|
36
|
+
Mao::Filter::Binary.new(:op => 'AND', :lhs => self, :rhs => rhs).freeze
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns an OR filter where the current object is the LHS and +rhs+ is the
|
40
|
+
# RHS.
|
41
|
+
def or(rhs)
|
42
|
+
Mao::Filter::Binary.new(:op => 'OR', :lhs => self, :rhs => rhs).freeze
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns an equality binary filter where the current object is the LHS and
|
46
|
+
# +rhs+ is the RHS.
|
47
|
+
def ==(rhs)
|
48
|
+
Mao::Filter::Binary.new(:op => '=', :lhs => self, :rhs => rhs)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns an inequality binary filter where the current object is the LHS and
|
52
|
+
# +rhs+ is the RHS.
|
53
|
+
def !=(rhs)
|
54
|
+
Mao::Filter::Binary.new(:op => '<>', :lhs => self, :rhs => rhs)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns a greater-than binary filter where the current object is the LHS
|
58
|
+
# and +rhs+ is the RHS.
|
59
|
+
def >(rhs)
|
60
|
+
Mao::Filter::Binary.new(:op => '>', :lhs => self, :rhs => rhs)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns a greater-than-or-equal-to binary filter where the current object
|
64
|
+
# is the LHS and +rhs+ is the RHS.
|
65
|
+
def >=(rhs)
|
66
|
+
Mao::Filter::Binary.new(:op => '>=', :lhs => self, :rhs => rhs)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns a less-than binary filter where the current object is the LHS and
|
70
|
+
# +rhs+ is the RHS.
|
71
|
+
def <(rhs)
|
72
|
+
Mao::Filter::Binary.new(:op => '<', :lhs => self, :rhs => rhs)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns a less-than-or-equal-to binary filter where the current object is
|
76
|
+
# the LHS and +rhs+ is the RHS.
|
77
|
+
def <=(rhs)
|
78
|
+
Mao::Filter::Binary.new(:op => '<=', :lhs => self, :rhs => rhs)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns a filter where the current object is checked if it IS NULL.
|
82
|
+
# HACK(arlen): ? Calling this "nil?" results in the world crashing down
|
83
|
+
# around us. But it seems a pity to have this be not-quite-like-Ruby. Would
|
84
|
+
# it be better to make #==(nil) map to IS NULL instead of = NULL?
|
85
|
+
def null?
|
86
|
+
Mao::Filter::Binary.new(:op => 'IS', :lhs => self, :rhs => nil)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns a filter where the current object is checked if it is IN +rhs+,
|
90
|
+
# typically a list.
|
91
|
+
def in(rhs)
|
92
|
+
Mao::Filter::Binary.new(:op => 'IN', :lhs => self, :rhs => rhs)
|
93
|
+
end
|
94
|
+
|
95
|
+
class Column
|
96
|
+
include Mao::Filter
|
97
|
+
|
98
|
+
def finalize
|
99
|
+
if @options[:table]
|
100
|
+
[:Column, @options[:table], @options[:name]]
|
101
|
+
else
|
102
|
+
[:Column, @options[:name]]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.sql(*opts)
|
107
|
+
opts.map {|i| Mao.quote_ident(i.to_s)}.join(".")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class Binary
|
112
|
+
include Mao::Filter
|
113
|
+
|
114
|
+
def finalize
|
115
|
+
[:Binary,
|
116
|
+
@options[:op],
|
117
|
+
Mao::Filter.finalize_or_literal(@options[:lhs]),
|
118
|
+
Mao::Filter.finalize_or_literal(@options[:rhs])]
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.sql(op, lhs, rhs)
|
122
|
+
s = "("
|
123
|
+
s << Mao::Filter.sql(lhs)
|
124
|
+
s << " "
|
125
|
+
s << op
|
126
|
+
s << " "
|
127
|
+
s << Mao::Filter.sql(rhs)
|
128
|
+
s << ")"
|
129
|
+
s
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# A context for the Mao::Query#where DSL, and for Mao::Query#join's table
|
134
|
+
# objects. Any non-lexically bound names hit WhereContext#method_missing,
|
135
|
+
# which checks if it belongs to a column, and if so, constructs a
|
136
|
+
# Mao::Filter::Column.
|
137
|
+
class Table
|
138
|
+
def initialize(query, explicit)
|
139
|
+
@query = query
|
140
|
+
@explicit = explicit
|
141
|
+
end
|
142
|
+
|
143
|
+
# Ensure +args+ and +block+ are both empty. Assert that a column for the
|
144
|
+
# query this context belongs to by the name +name+ exists, and return a
|
145
|
+
# Mao::Filter::Column for that column.
|
146
|
+
def method_missing(name, *args, &block)
|
147
|
+
if args.length > 0
|
148
|
+
raise ArgumentError, "args not expected in #where subclause"
|
149
|
+
end
|
150
|
+
|
151
|
+
if block
|
152
|
+
raise ArgumentError, "block not expected in #where subclause"
|
153
|
+
end
|
154
|
+
|
155
|
+
unless @query.col_types[name]
|
156
|
+
raise ArgumentError, "unknown column for #{@query.table}: #{name}"
|
157
|
+
end
|
158
|
+
|
159
|
+
if @explicit
|
160
|
+
Column.new(:table => @query.table.to_sym, :name => name).freeze
|
161
|
+
else
|
162
|
+
Column.new(:name => name).freeze
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# vim: set sw=2 cc=80 et:
|
data/lib/mao/query.rb
ADDED
@@ -0,0 +1,381 @@
|
|
1
|
+
# A persistent query structure which can be manipulated by creating new
|
2
|
+
# persistent queries, and eventually executed.
|
3
|
+
#
|
4
|
+
# All "state" about the query itself is deliberately stored in a simple Hash,
|
5
|
+
# @options, to provide transparency and ensure simplicity of the resulting
|
6
|
+
# design.
|
7
|
+
class Mao::Query
|
8
|
+
require 'mao/filter'
|
9
|
+
|
10
|
+
# A container for text that should be inserted raw into a query.
|
11
|
+
class Raw
|
12
|
+
def initialize(text)
|
13
|
+
@text = text
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :text
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns a Mao::Query::Raw with +text+.
|
20
|
+
def self.raw(text)
|
21
|
+
Raw.new(text).freeze
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(table, options={}, col_types=nil)
|
25
|
+
@table, @options = table.to_sym, options.freeze
|
26
|
+
|
27
|
+
if !col_types
|
28
|
+
col_types = {}
|
29
|
+
Mao.sql(
|
30
|
+
'SELECT column_name, data_type FROM information_schema.columns ' \
|
31
|
+
'WHERE table_name=$1',
|
32
|
+
[@table.to_s]) do |pg_result|
|
33
|
+
if pg_result.num_tuples.zero?
|
34
|
+
raise ArgumentError, "invalid or blank table #@table"
|
35
|
+
end
|
36
|
+
|
37
|
+
pg_result.each do |tuple|
|
38
|
+
col_types[tuple["column_name"].to_sym] = tuple["data_type"]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
@col_types = col_types.freeze
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_reader :table
|
47
|
+
attr_reader :options
|
48
|
+
attr_reader :col_types
|
49
|
+
|
50
|
+
# Returns a new Mao::Query with +options+ merged into the options of this
|
51
|
+
# object.
|
52
|
+
def with_options(options)
|
53
|
+
self.class.new(@table, @options.merge(options), @col_types).freeze
|
54
|
+
end
|
55
|
+
|
56
|
+
# Restricts the query to at most +n+ results.
|
57
|
+
def limit(n)
|
58
|
+
unless n.is_a? Integer
|
59
|
+
raise ArgumentError, "#{n.inspect} not an Integer"
|
60
|
+
end
|
61
|
+
|
62
|
+
with_options(:limit => n.to_i)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns the query's results sorted by +column+ in +direction+, either :asc
|
66
|
+
# or :desc.
|
67
|
+
def order(column, direction)
|
68
|
+
unless column.is_a?(Symbol) and [:asc, :desc].include?(direction)
|
69
|
+
raise ArgumentError,
|
70
|
+
"#{column.inspect} not a Symbol or " \
|
71
|
+
"#{direction.inspect} not :asc or :desc"
|
72
|
+
end
|
73
|
+
|
74
|
+
check_column(column, @table, @col_types)
|
75
|
+
|
76
|
+
direction = direction == :asc ? "ASC" : "DESC"
|
77
|
+
with_options(:order => [column, direction])
|
78
|
+
end
|
79
|
+
|
80
|
+
# Only returns the given +columns+, Symbols (possibly nested in Arrays).
|
81
|
+
#
|
82
|
+
# If +columns+ is a single argument, and it's a Hash, the keys should be
|
83
|
+
# Symbols corresponding to table names, and the values Arrays of Symbol
|
84
|
+
# column names. This is only for use with #join, and #only must be called
|
85
|
+
# after #join.
|
86
|
+
def only(*columns)
|
87
|
+
columns = columns.flatten
|
88
|
+
|
89
|
+
if columns.length == 1 and columns[0].is_a?(Hash)
|
90
|
+
unless @options[:join]
|
91
|
+
raise ArgumentError, "#only with a Hash must be used only after #join"
|
92
|
+
end
|
93
|
+
|
94
|
+
other = Mao.query(@options[:join][0])
|
95
|
+
columns = columns[0]
|
96
|
+
columns.each do |table, table_columns|
|
97
|
+
unless table_columns.is_a? Array
|
98
|
+
raise ArgumentError, "#{table_columns.inspect} is not an Array"
|
99
|
+
end
|
100
|
+
|
101
|
+
if table == @table
|
102
|
+
table_columns.each do |column|
|
103
|
+
check_column(column, @table, @col_types)
|
104
|
+
end
|
105
|
+
elsif table == other.table
|
106
|
+
table_columns.each do |column|
|
107
|
+
check_column(column, other.table, other.col_types)
|
108
|
+
end
|
109
|
+
else
|
110
|
+
raise ArgumentError, "#{table} is not a column in this query"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
else
|
114
|
+
columns.each do |column|
|
115
|
+
check_column(column, @table, @col_types)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
with_options(:only => columns)
|
120
|
+
end
|
121
|
+
|
122
|
+
# For INSERTs, returns +columns+ for inserted rows.
|
123
|
+
def returning(*columns)
|
124
|
+
columns = columns.flatten
|
125
|
+
|
126
|
+
columns.each do |column|
|
127
|
+
check_column(column, @table, @col_types)
|
128
|
+
end
|
129
|
+
|
130
|
+
with_options(:returning => columns)
|
131
|
+
end
|
132
|
+
|
133
|
+
# A context for the #join DSL. Any non-lexically bound names hit
|
134
|
+
# JoinContext#method_missing, which constructs a Mao::Filter::Table for the
|
135
|
+
# table with that name.
|
136
|
+
class JoinContext
|
137
|
+
# Ensure +args+ and +block+ are both empty. Creates a Mao::Query for the
|
138
|
+
# name invoked, which ensures such a table exists. Assuming it exists, a
|
139
|
+
# Mao::Filter::Table for that query is constructed.
|
140
|
+
def method_missing(name, *args, &block)
|
141
|
+
if args.length > 0
|
142
|
+
raise ArgumentError, "args not expected in #where subclause"
|
143
|
+
end
|
144
|
+
|
145
|
+
if block
|
146
|
+
raise ArgumentError, "block not expected in #where subclause"
|
147
|
+
end
|
148
|
+
|
149
|
+
Mao::Filter::Table.new(Mao.query(name), true).freeze
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Filters results based on the conditions specified in +block+.
|
154
|
+
#
|
155
|
+
# Depending on if #join has been called, one of two things occur:
|
156
|
+
# 1. If #join has not been called, +block+ has available in context the
|
157
|
+
# column names of the table being queried; or,
|
158
|
+
# 2. If #join has been called, +block+ has available in context the table
|
159
|
+
# names of the tables involved in the query. Per #join, those objects
|
160
|
+
# will have the columns available as methods.
|
161
|
+
#
|
162
|
+
# Once you have a column, use regular operators to construct tests, e.g. "x
|
163
|
+
# == 3" will filter where the column "x" has value 3.
|
164
|
+
#
|
165
|
+
# Boolean operations on columns return Mao::Filter objects; use #and and #or
|
166
|
+
# to combine them. The return value of the block should be the full desired
|
167
|
+
# filter.
|
168
|
+
def where(&block)
|
169
|
+
if @options[:join]
|
170
|
+
context = JoinContext.new.freeze
|
171
|
+
else
|
172
|
+
context = Mao::Filter::Table.new(self, false).freeze
|
173
|
+
end
|
174
|
+
|
175
|
+
with_options(:where => context.instance_exec(&block).finalize)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Joins the results of this table against another table, +target+. The
|
179
|
+
# conditions for joining one row in this table against one in +target+ are
|
180
|
+
# specified in +block+.
|
181
|
+
#
|
182
|
+
# +block+ is per #where's, except the names in context are tables, not
|
183
|
+
# columns; the tables returned are the same as the context of #where itself,
|
184
|
+
# so for instance, "blah.x == 3" will filter where the column "x" of table
|
185
|
+
# "blah" (which should be either this table, or the +target+ table) equals 3.
|
186
|
+
#
|
187
|
+
# Boolean operations are then all per #where.
|
188
|
+
#
|
189
|
+
# If +block+ is not specified, +target+ is instead treated as a Hash of the
|
190
|
+
# form {foreign_table => {local_key => foreign_key}}.
|
191
|
+
def join(target, &block)
|
192
|
+
if !block
|
193
|
+
local_table = @table
|
194
|
+
foreign_table = target.keys[0]
|
195
|
+
mapping = target[foreign_table]
|
196
|
+
local_key = mapping.keys[0]
|
197
|
+
foreign_key = mapping[local_key]
|
198
|
+
return join(foreign_table) {
|
199
|
+
send(local_table).send(local_key) ==
|
200
|
+
send(foreign_table).send(foreign_key)
|
201
|
+
}
|
202
|
+
end
|
203
|
+
|
204
|
+
context = JoinContext.new.freeze
|
205
|
+
|
206
|
+
with_options(:join => [target, context.instance_exec(&block).finalize])
|
207
|
+
end
|
208
|
+
|
209
|
+
# Constructs the SQL for this query.
|
210
|
+
def sql
|
211
|
+
s = ""
|
212
|
+
options = @options.dup
|
213
|
+
|
214
|
+
if update = options.delete(:update)
|
215
|
+
s = "UPDATE "
|
216
|
+
s << Mao.quote_ident(@table)
|
217
|
+
s << " SET "
|
218
|
+
|
219
|
+
if update.length == 0
|
220
|
+
raise ArgumentError, "invalid update: nothing to set"
|
221
|
+
end
|
222
|
+
|
223
|
+
s << update.map do |column, value|
|
224
|
+
check_column(column, @table, @col_types)
|
225
|
+
|
226
|
+
"#{Mao.quote_ident(column)} = #{Mao.escape_literal(value)}"
|
227
|
+
end.join(", ")
|
228
|
+
|
229
|
+
if where = options.delete(:where)
|
230
|
+
s << " WHERE "
|
231
|
+
s << Mao::Filter.sql(where)
|
232
|
+
end
|
233
|
+
elsif insert = options.delete(:insert)
|
234
|
+
s = "INSERT INTO "
|
235
|
+
s << Mao.quote_ident(@table)
|
236
|
+
s << " ("
|
237
|
+
|
238
|
+
keys = insert.map(&:keys).flatten.uniq.sort
|
239
|
+
s << keys.map do |column|
|
240
|
+
check_column(column, @table, @col_types)
|
241
|
+
Mao.quote_ident(column)
|
242
|
+
end.join(", ")
|
243
|
+
s << ") VALUES "
|
244
|
+
|
245
|
+
first = true
|
246
|
+
insert.each do |row|
|
247
|
+
if first
|
248
|
+
first = false
|
249
|
+
else
|
250
|
+
s << ", "
|
251
|
+
end
|
252
|
+
|
253
|
+
s << "("
|
254
|
+
s << keys.map {|k|
|
255
|
+
if row.include?(k)
|
256
|
+
Mao.escape_literal(row[k])
|
257
|
+
else
|
258
|
+
"DEFAULT"
|
259
|
+
end
|
260
|
+
}.join(", ")
|
261
|
+
s << ")"
|
262
|
+
end
|
263
|
+
|
264
|
+
if returning = options.delete(:returning)
|
265
|
+
s << " RETURNING "
|
266
|
+
s << returning.map {|c| Mao.quote_ident(c)}.join(", ")
|
267
|
+
end
|
268
|
+
else
|
269
|
+
s = "SELECT "
|
270
|
+
|
271
|
+
join = options.delete(:join)
|
272
|
+
only = options.delete(:only)
|
273
|
+
|
274
|
+
if join
|
275
|
+
n = 0
|
276
|
+
s << (@col_types.keys.sort.map {|c|
|
277
|
+
n += 1
|
278
|
+
if !only or (only[@table] and only[@table].include?(c))
|
279
|
+
"#{Mao.quote_ident(@table)}.#{Mao.quote_ident(c)} " +
|
280
|
+
"#{Mao.quote_ident("c#{n}")}"
|
281
|
+
end
|
282
|
+
} + Mao.query(join[0]).col_types.keys.sort.map {|c|
|
283
|
+
n += 1
|
284
|
+
if !only or (only[join[0]] and only[join[0]].include?(c))
|
285
|
+
"#{Mao.quote_ident(join[0])}.#{Mao.quote_ident(c)} " +
|
286
|
+
"#{Mao.quote_ident("c#{n}")}"
|
287
|
+
end
|
288
|
+
}).reject(&:nil?).join(", ")
|
289
|
+
elsif only
|
290
|
+
s << only.map {|c| Mao.quote_ident(c)}.join(", ")
|
291
|
+
else
|
292
|
+
s << "*"
|
293
|
+
end
|
294
|
+
|
295
|
+
s << " FROM #{Mao.quote_ident(@table)}"
|
296
|
+
|
297
|
+
if join
|
298
|
+
s << " INNER JOIN #{Mao.quote_ident(join[0])} ON "
|
299
|
+
s << Mao::Filter.sql(join[1])
|
300
|
+
end
|
301
|
+
|
302
|
+
if where = options.delete(:where)
|
303
|
+
s << " WHERE "
|
304
|
+
s << Mao::Filter.sql(where)
|
305
|
+
end
|
306
|
+
|
307
|
+
if order = options.delete(:order)
|
308
|
+
s << " ORDER BY "
|
309
|
+
s << Mao.quote_ident(order[0])
|
310
|
+
s << " "
|
311
|
+
s << order[1]
|
312
|
+
end
|
313
|
+
|
314
|
+
if limit = options.delete(:limit)
|
315
|
+
s << " LIMIT #{limit}"
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
if options.length > 0
|
320
|
+
raise ArgumentError,
|
321
|
+
"invalid options in #sql: #{options.inspect}. " \
|
322
|
+
"SQL constructed: #{s}"
|
323
|
+
end
|
324
|
+
|
325
|
+
s
|
326
|
+
end
|
327
|
+
|
328
|
+
# Executes the constructed query and returns an Array of Hashes of results.
|
329
|
+
def select!
|
330
|
+
# Ensure we can never be destructive by nilifying :update.
|
331
|
+
Mao.sql(with_options(:update => nil).sql) do |pg_result|
|
332
|
+
if @options[:join]
|
333
|
+
other = Mao.query(@options[:join][0])
|
334
|
+
pg_result.map {|result|
|
335
|
+
Mao.normalize_join_result(result, self, other)
|
336
|
+
}
|
337
|
+
else
|
338
|
+
pg_result.map {|result| Mao.normalize_result(result, @col_types)}
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
# Limits the query to one result, and returns that result.
|
344
|
+
def select_first!
|
345
|
+
limit(1).select!.first
|
346
|
+
end
|
347
|
+
|
348
|
+
# Executes the changes in Hash +changes+ to the rows matching this object,
|
349
|
+
# returning the number of affected rows.
|
350
|
+
def update!(changes)
|
351
|
+
Mao.sql(with_options(:update => changes).sql) do |pg_result|
|
352
|
+
pg_result.cmd_tuples
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# Inserts +rows+ into the table. No other options should be applied to this
|
357
|
+
# query. Returns the number of inserted rows, unless #returning was called,
|
358
|
+
# in which case the calculated values from the INSERT are returned.
|
359
|
+
def insert!(*rows)
|
360
|
+
Mao.sql(with_options(:insert => rows.flatten).sql) do |pg_result|
|
361
|
+
if @options[:returning]
|
362
|
+
pg_result.map {|result| Mao.normalize_result(result, @col_types)}
|
363
|
+
else
|
364
|
+
pg_result.cmd_tuples
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
private
|
370
|
+
|
371
|
+
def check_column column, table, col_types
|
372
|
+
unless column.is_a? Symbol
|
373
|
+
raise ArgumentError, "#{column.inspect} not a Symbol"
|
374
|
+
end
|
375
|
+
unless col_types[column]
|
376
|
+
raise ArgumentError, "#{column.inspect} is not a column in #{table}"
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
# vim: set sw=2 cc=80 et:
|