kansas 0.9.0
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.
- checksums.yaml +7 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +97 -0
- data/Rakefile +11 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/kansas.gemspec +35 -0
- data/lib/kansas.rb +42 -0
- data/lib/kansas/BelongsTo.rb +23 -0
- data/lib/kansas/Database.rb +659 -0
- data/lib/kansas/Expression.rb +361 -0
- data/lib/kansas/Table.rb +102 -0
- data/lib/kansas/TableClass.rb +179 -0
- data/lib/kansas/ToMany.rb +13 -0
- data/lib/kansas/ToOne.rb +29 -0
- data/lib/kansas/adaptors/AdaptorRule.rb +17 -0
- data/lib/kansas/adaptors/Adaptors.rb +24 -0
- data/lib/kansas/adaptors/StandardSQLMixin.rb +106 -0
- data/lib/kansas/adaptors/dbi_rule.rb +8 -0
- data/lib/kansas/patch_dbi.rb +54 -0
- data/lib/kansas/version.rb +3 -0
- data/notes +85 -0
- metadata +138 -0
@@ -0,0 +1,361 @@
|
|
1
|
+
class Object
|
2
|
+
def expr_body
|
3
|
+
to_s
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
class Array
|
8
|
+
def expr_body
|
9
|
+
collect {|e| e.expr_body}.join(',')
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class String
|
14
|
+
def expr_body
|
15
|
+
sql_escape(self)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class KSExpression
|
20
|
+
include DRbUndumped
|
21
|
+
|
22
|
+
class Context
|
23
|
+
|
24
|
+
attr_reader :tables, :joins, :select, :select_table, :sort_fields, :limits, :distinct
|
25
|
+
|
26
|
+
def initialize(*tables)
|
27
|
+
# @select = tables.collect {|t| t.table_name}
|
28
|
+
@select = tables[0].table_name
|
29
|
+
@select_table = tables[0]
|
30
|
+
@tables = []
|
31
|
+
@joins = []
|
32
|
+
@sort_fields = []
|
33
|
+
@limits = []
|
34
|
+
@distinct = {}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def select_sql
|
39
|
+
distinct_fields = []
|
40
|
+
fields = []
|
41
|
+
@context.select_table.fields.each_value do |f|
|
42
|
+
if @context.distinct[f]
|
43
|
+
distinct_fields.push "distinct(#{@context.select}.#{f})"
|
44
|
+
else
|
45
|
+
fields.push "#{@context.select}.#{f}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
fields = distinct_fields.concat(fields)
|
50
|
+
|
51
|
+
selectedTables = @context.tables.compact.flatten.uniq.join(',')
|
52
|
+
joinConstraints = @context.joins.compact.flatten.uniq.join(' AND ')
|
53
|
+
#selected_rows = @context.select.compact.flatten.uniq.collect {|t| "#{t}.*"}.join(',')
|
54
|
+
|
55
|
+
if joinConstraints != ""
|
56
|
+
joinConstraints << " AND "
|
57
|
+
end
|
58
|
+
|
59
|
+
#statement = "SELECT #{@context.select}.* FROM #{selectedTables} WHERE #{joinConstraints} #{expr_body}"
|
60
|
+
statement = "SELECT #{fields.join(',')} FROM #{selectedTables} WHERE #{joinConstraints} #{expr_body}"
|
61
|
+
statement << ' ORDER BY ' << @context.sort_fields.collect {|f| "#{f[0].respond_to?(:expr_body) ? f[0].expr_body : f[0].to_s} #{f[1]}"}.join(',') if @context.sort_fields.length > 0
|
62
|
+
statement << ' LIMIT ' << @context.limits.join(',') if @context.limits.length > 0
|
63
|
+
|
64
|
+
statement
|
65
|
+
end
|
66
|
+
|
67
|
+
alias :sql :select_sql
|
68
|
+
|
69
|
+
def count_sql
|
70
|
+
selectedTables = @context.tables.compact.flatten.uniq.join(',')
|
71
|
+
joinConstraints = @context.joins.compact.flatten.uniq.join(' AND ')
|
72
|
+
#selected_rows = @context.select.compact.flatten.uniq.collect {|t| "#{t}.*"}.join(',')
|
73
|
+
|
74
|
+
if joinConstraints != ""
|
75
|
+
joinConstraints << " AND "
|
76
|
+
end
|
77
|
+
|
78
|
+
statement = "SELECT count(*) FROM #{selectedTables} WHERE #{joinConstraints} #{expr_body}"
|
79
|
+
statement << ' ORDER BY ' << @context.sort_fields.collect {|f| "#{f[0].respond_to?(:expr_body) ? f[0].expr_body : f[0].to_s} #{f[1]}"}.join(',') if @context.sort_fields.length > 0
|
80
|
+
statement << ' LIMIT ' << @context.limits.join(',') if @context.limits.length > 0
|
81
|
+
|
82
|
+
statement
|
83
|
+
end
|
84
|
+
|
85
|
+
def delete_sql
|
86
|
+
selectedTables = @context.tables.compact.flatten.uniq.join(",")
|
87
|
+
joinConstraints = @context.joins.compact.flatten.uniq.join(" AND ")
|
88
|
+
if joinConstraints != ""
|
89
|
+
joinConstraints << " AND "
|
90
|
+
end
|
91
|
+
|
92
|
+
statement = "DELETE FROM #{selectedTables} WHERE #{joinConstraints} #{expr_body}"
|
93
|
+
|
94
|
+
statement
|
95
|
+
end
|
96
|
+
|
97
|
+
def KSExpression.operator(name, keyword, op=nil)
|
98
|
+
opClass = Class.new(KSBinaryOperator)
|
99
|
+
opClass.setKeyword(keyword)
|
100
|
+
const_set(name, opClass)
|
101
|
+
|
102
|
+
fn = op ? op : name
|
103
|
+
class_eval <<-EOS
|
104
|
+
define_method(:"#{fn}") {|val| #{name}.new(self, val, @context) }
|
105
|
+
EOS
|
106
|
+
alias_method name, op if op
|
107
|
+
end
|
108
|
+
|
109
|
+
def KSExpression.binary_function(name, keyword, op = nil, class_to_use = KSFunctionOperator)
|
110
|
+
opClass = Class.new(class_to_use)
|
111
|
+
opClass.setKeyword(keyword)
|
112
|
+
const_set(name,opClass)
|
113
|
+
|
114
|
+
class_eval <<-EOS
|
115
|
+
define_method(:"#{name}") {|*val| #{name}.new(self,val,@context) }
|
116
|
+
EOS
|
117
|
+
alias_method op, name if op
|
118
|
+
end
|
119
|
+
|
120
|
+
def KSExpression.unary_function(name, keyword, op = nil, class_to_use = KSUnaryFunction)
|
121
|
+
opClass = Class.new(KSUnaryFunction)
|
122
|
+
opClass.setKeyword(keyword)
|
123
|
+
const_set(name,opClass)
|
124
|
+
|
125
|
+
class_eval <<-EOS
|
126
|
+
define_method(:"#{name}") {|*val| #{name}.new(val, @context) }
|
127
|
+
EOS
|
128
|
+
alias_method op, name if op
|
129
|
+
end
|
130
|
+
|
131
|
+
def KSExpression.unary_operator(name, keyword, op=nil)
|
132
|
+
opClass = Class.new(KSUnaryOperator)
|
133
|
+
opClass.setKeyword(keyword)
|
134
|
+
const_set(name, opClass)
|
135
|
+
|
136
|
+
class_eval <<-EOS
|
137
|
+
define_method(:"#{name}") { #{name}.new(self, @context) }
|
138
|
+
EOS
|
139
|
+
|
140
|
+
alias_method op, name if op
|
141
|
+
end
|
142
|
+
|
143
|
+
class KSOperator < KSExpression
|
144
|
+
|
145
|
+
def KSOperator.setKeyword(k)
|
146
|
+
@keyword = k
|
147
|
+
end
|
148
|
+
|
149
|
+
def KSOperator.keyword
|
150
|
+
@keyword
|
151
|
+
end
|
152
|
+
|
153
|
+
def keyword
|
154
|
+
self.class.keyword
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
class KSBinaryOperator < KSOperator
|
159
|
+
def initialize(a, b, context)
|
160
|
+
@a, @b, @context = a, b, context
|
161
|
+
end
|
162
|
+
|
163
|
+
def expr_body
|
164
|
+
"(#{@a.expr_body} #{keyword} #{@b.expr_body})"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
class KSUnaryOperator < KSOperator
|
169
|
+
def initialize(a, context)
|
170
|
+
@a, @context = a, context
|
171
|
+
end
|
172
|
+
|
173
|
+
def expr_body
|
174
|
+
"#{@a.expr_body} #{keyword}"
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
class KSUnaryFunction < KSOperator
|
179
|
+
def initialize(a, context)
|
180
|
+
@a, @context = a, context
|
181
|
+
end
|
182
|
+
|
183
|
+
def expr_body
|
184
|
+
"#{keyword}(#{@a.expr_body})"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
class KSFunctionOperator < KSOperator
|
189
|
+
def initialize(a,b,context)
|
190
|
+
@a, @b, @context = a, b, context
|
191
|
+
end
|
192
|
+
|
193
|
+
def expr_body
|
194
|
+
"#{@a.expr_body} #{keyword}(#{@b.expr_body})"
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
class KSBetweenFunction < KSFunctionOperator
|
199
|
+
def expr_body
|
200
|
+
"#{@a.expr_body} #{keyword} #{@b[0].expr_body} AND #{@b[1].expr_body}"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
|
206
|
+
class KSTableExpr < KSExpression
|
207
|
+
|
208
|
+
def initialize(table, context = nil)
|
209
|
+
@table = table
|
210
|
+
@context = context ? context : KSExpression::Context.new(table)
|
211
|
+
@context.tables << table.table_name
|
212
|
+
end
|
213
|
+
|
214
|
+
def sort_by(*exprs)
|
215
|
+
exprs.each do |sort_field|
|
216
|
+
if Hash === sort_field
|
217
|
+
sort_field.each_pair do |k,v|
|
218
|
+
/desc/i.match(v) ? 'DESC' : 'ASC'
|
219
|
+
@context.sort_fields.push [k,v]
|
220
|
+
end
|
221
|
+
else
|
222
|
+
@context.sort_fields.push [sort_field,'ASC']
|
223
|
+
end
|
224
|
+
end
|
225
|
+
KSTrueExpr.new(@context)
|
226
|
+
end
|
227
|
+
alias :order_by :sort_by
|
228
|
+
|
229
|
+
def limit(*exprs)
|
230
|
+
exprs.each do |e|
|
231
|
+
@context.limits.push e
|
232
|
+
end
|
233
|
+
KSTrueExpr.new(@context)
|
234
|
+
end
|
235
|
+
|
236
|
+
def distinct(*exprs)
|
237
|
+
exprs.each do |e|
|
238
|
+
@context.distinct[e.field] = true
|
239
|
+
end
|
240
|
+
KSTrueExpr.new(@context)
|
241
|
+
end
|
242
|
+
|
243
|
+
def field(name, *args, &block)
|
244
|
+
if match = /^_(.*)/.match(name.to_s)
|
245
|
+
func = match[1]
|
246
|
+
KSFuncExpr.new(@table,func,@context,args)
|
247
|
+
elsif field = @table.fields[name.to_s]
|
248
|
+
KSFieldExpr.new(@table, field, @context)
|
249
|
+
elsif @table.relations and relation = @table.relations[name.to_s]
|
250
|
+
@context.joins << relation.join
|
251
|
+
KSTableExpr.new(relation.foreignTable, @context)
|
252
|
+
else
|
253
|
+
meth = KSExpression.method(name.to_s)
|
254
|
+
meth.call(args, &block)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def respond_to?(method)
|
259
|
+
if match = /^_(.*)/.match(method.to_s)
|
260
|
+
true
|
261
|
+
elsif field = @table.fields[method.to_s]
|
262
|
+
true
|
263
|
+
elsif @table.relations and relation = @table.relations[method.to_s]
|
264
|
+
true
|
265
|
+
else
|
266
|
+
KSExpression.respond_to?(method.to_s)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def method_missing(method, *args)
|
271
|
+
# _blahblah() indicates that blahblah is a function to be invoked
|
272
|
+
# on the database side. This is a bit of a hack, but I don't have a
|
273
|
+
# better solution in my head at the moment.
|
274
|
+
if match = /^_(.*)/.match(method.to_s)
|
275
|
+
func = match[1]
|
276
|
+
KSFuncExpr.new(@table,func,@context,args)
|
277
|
+
elsif field = @table.fields[method.to_s]
|
278
|
+
KSFieldExpr.new(@table, field, @context)
|
279
|
+
elsif @table.relations and relation = @table.relations[method.to_s]
|
280
|
+
@context.joins << relation.join
|
281
|
+
KSTableExpr.new(relation.foreignTable, @context)
|
282
|
+
elsif KSExpression.respond_to?(method.to_s)
|
283
|
+
meth = KSExpression.method(method.to_s)
|
284
|
+
meth.call(args)
|
285
|
+
else
|
286
|
+
raise KSBadFieldName,"KSBadFieldName: '#{method}' is not a valid field name"
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def expr_body
|
291
|
+
@table
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
class KSTrueExpr < KSExpression
|
296
|
+
def initialize(context)
|
297
|
+
@context = context
|
298
|
+
end
|
299
|
+
|
300
|
+
def expr_body
|
301
|
+
'1'
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
class KSFieldExpr < KSExpression
|
306
|
+
|
307
|
+
def initialize(table, field, context)
|
308
|
+
@table, @field, @context = table, field, context
|
309
|
+
end
|
310
|
+
|
311
|
+
def field
|
312
|
+
@field
|
313
|
+
end
|
314
|
+
|
315
|
+
def expr_body
|
316
|
+
"#{@table.table_name}.#{@field}"
|
317
|
+
end
|
318
|
+
|
319
|
+
alias :old_respond_to? :respond_to?
|
320
|
+
def respond_to?(method)
|
321
|
+
old_respond_to?(method)
|
322
|
+
end
|
323
|
+
|
324
|
+
def method_missing(method,*args,&block)
|
325
|
+
super(method,*args,&block)
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
class KSFuncExpr < KSExpression
|
330
|
+
def initialize(table, func, context, args)
|
331
|
+
@table, @func, @context, @args = table, func, context, args
|
332
|
+
end
|
333
|
+
|
334
|
+
def expr_body
|
335
|
+
"#{@func}(#{@args.join(',')})"
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
# These are the recognized relational operators.
|
340
|
+
|
341
|
+
KSExpression.operator(:AND, "AND", :&)
|
342
|
+
KSExpression.operator(:OR, "OR", :|)
|
343
|
+
KSExpression.operator(:LT, "<", :<)
|
344
|
+
KSExpression.operator(:GT, ">", :>)
|
345
|
+
KSExpression.operator(:LTE, "<=", :<=)
|
346
|
+
KSExpression.operator(:GTE, ">=", :>=)
|
347
|
+
KSExpression.operator(:LIKE, "LIKE", :=~)
|
348
|
+
KSExpression.operator(:EQ, "=", :==)
|
349
|
+
KSExpression.operator(:NEQ, "<=>", :<=>)
|
350
|
+
KSExpression.operator(:NOTEQ, "!=", :noteq)
|
351
|
+
KSExpression.operator(:NOTEQ2, "!=", :'!=')
|
352
|
+
KSExpression.unary_operator(:IS_NULL, "IS NULL", :is_null)
|
353
|
+
KSExpression.unary_operator(:IS_NOT_NULL, "IS NOT NULL", :is_not_null)
|
354
|
+
KSExpression.binary_function(:IN, "IN", :in)
|
355
|
+
KSExpression.binary_function(:BETWEEN, "BETWEEN", :between, KSExpression::KSBetweenFunction)
|
356
|
+
KSExpression.binary_function(:NOT_IN, "NOT IN", :not_in)
|
357
|
+
KSExpression.binary_function(:NOT_BETWEEN, "NOT BETWEEN", :not_between, KSExpression::KSBetweenFunction)
|
358
|
+
KSExpression.unary_function(:GREATEST, "GREATEST", :greatest)
|
359
|
+
KSExpression.unary_function(:LEAST, "LEAST", :least)
|
360
|
+
KSExpression.unary_function(:MIN, "MIN", :min)
|
361
|
+
KSExpression.unary_function(:MAX, "MAX", :max)
|
data/lib/kansas/Table.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
class KSTable
|
2
|
+
include DRbUndumped
|
3
|
+
|
4
|
+
|
5
|
+
attr_accessor :row, :context, :rollback_buffer, :rollback_hash
|
6
|
+
|
7
|
+
def pending_deletion?
|
8
|
+
@deletion
|
9
|
+
end
|
10
|
+
|
11
|
+
def read_only?
|
12
|
+
@read_only
|
13
|
+
end
|
14
|
+
alias :read_only :read_only?
|
15
|
+
|
16
|
+
def read_only=(cond)
|
17
|
+
@read_only = cond ? true : false
|
18
|
+
end
|
19
|
+
|
20
|
+
def serialized?
|
21
|
+
@serialized
|
22
|
+
end
|
23
|
+
|
24
|
+
def set_pending_deletion
|
25
|
+
@deletion = true
|
26
|
+
end
|
27
|
+
|
28
|
+
def reset_pending_deletion
|
29
|
+
@deletion = false
|
30
|
+
end
|
31
|
+
|
32
|
+
def set_serialized
|
33
|
+
@serialized = true
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize
|
37
|
+
@row = {}
|
38
|
+
@rollback_buffer = []
|
39
|
+
@rollback_hash = {}
|
40
|
+
@serialized = false
|
41
|
+
@pending_deletion = false
|
42
|
+
@read_only = false
|
43
|
+
end
|
44
|
+
|
45
|
+
def load(row, context = nil, read_only = false)
|
46
|
+
@row, @context, @read_only = row, context, read_only
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def key
|
51
|
+
result = self.class.primaries.collect{|f| @row[f.to_s]}
|
52
|
+
end
|
53
|
+
|
54
|
+
def changed
|
55
|
+
@context.changed(self) if defined?(@context) && @context
|
56
|
+
end
|
57
|
+
|
58
|
+
def table_name
|
59
|
+
self.class.table_name
|
60
|
+
end
|
61
|
+
|
62
|
+
def inspect
|
63
|
+
table_name + @row.inspect
|
64
|
+
end
|
65
|
+
|
66
|
+
def delete # TODO: Add a cascade_delete that deletes the row plus any to_many relations to the row.
|
67
|
+
if @context.autocommit?
|
68
|
+
@context.delete_one(self)
|
69
|
+
else
|
70
|
+
set_pending_deletion
|
71
|
+
changed
|
72
|
+
nullify_self
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def nullify_self
|
77
|
+
@rollback_buffer.push self.dup
|
78
|
+
@row.each_key do |key|
|
79
|
+
@rollback_hash[key] ||= []
|
80
|
+
@rollback_hash[key].push @row[key]
|
81
|
+
end
|
82
|
+
|
83
|
+
@row = {}
|
84
|
+
end
|
85
|
+
# Unwind the changes.
|
86
|
+
|
87
|
+
def rollback
|
88
|
+
@rollback_buffer.reverse.each do |rbval|
|
89
|
+
if Array === rbval
|
90
|
+
@row[rbval[0]] = rbval[1]
|
91
|
+
else
|
92
|
+
@row = rbval.row
|
93
|
+
reset_pending_deletion
|
94
|
+
end
|
95
|
+
end
|
96
|
+
@rollback_buffer.clear
|
97
|
+
@rollback_hash.each_key do |key|
|
98
|
+
@rollback_hash[key].clear
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|