og 0.20.0 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/CHANGELOG +796 -664
  2. data/INSTALL +24 -24
  3. data/README +39 -32
  4. data/Rakefile +41 -42
  5. data/benchmark/bench.rb +36 -36
  6. data/doc/AUTHORS +15 -12
  7. data/doc/LICENSE +3 -3
  8. data/doc/RELEASES +311 -243
  9. data/doc/config.txt +1 -1
  10. data/examples/mysql_to_psql.rb +15 -15
  11. data/examples/run.rb +92 -92
  12. data/install.rb +7 -17
  13. data/lib/og.rb +76 -75
  14. data/lib/og/collection.rb +203 -160
  15. data/lib/og/entity.rb +168 -169
  16. data/lib/og/errors.rb +5 -5
  17. data/lib/og/manager.rb +179 -178
  18. data/lib/og/mixin/hierarchical.rb +107 -107
  19. data/lib/og/mixin/optimistic_locking.rb +36 -36
  20. data/lib/og/mixin/orderable.rb +148 -148
  21. data/lib/og/mixin/timestamped.rb +8 -8
  22. data/lib/og/mixin/tree.rb +124 -124
  23. data/lib/og/relation.rb +237 -213
  24. data/lib/og/relation/belongs_to.rb +5 -5
  25. data/lib/og/relation/has_many.rb +60 -58
  26. data/lib/og/relation/joins_many.rb +93 -47
  27. data/lib/og/relation/refers_to.rb +25 -21
  28. data/lib/og/store.rb +210 -207
  29. data/lib/og/store/filesys.rb +79 -79
  30. data/lib/og/store/kirby.rb +263 -258
  31. data/lib/og/store/memory.rb +261 -261
  32. data/lib/og/store/mysql.rb +288 -284
  33. data/lib/og/store/psql.rb +261 -244
  34. data/lib/og/store/sql.rb +873 -720
  35. data/lib/og/store/sqlite.rb +177 -175
  36. data/lib/og/store/sqlserver.rb +204 -214
  37. data/lib/og/types.rb +1 -1
  38. data/lib/og/validation.rb +57 -57
  39. data/lib/vendor/mysql.rb +376 -376
  40. data/lib/vendor/mysql411.rb +10 -10
  41. data/test/og/mixin/tc_hierarchical.rb +59 -59
  42. data/test/og/mixin/tc_optimistic_locking.rb +40 -40
  43. data/test/og/mixin/tc_orderable.rb +67 -67
  44. data/test/og/mixin/tc_timestamped.rb +19 -19
  45. data/test/og/store/tc_filesys.rb +46 -46
  46. data/test/og/tc_inheritance.rb +81 -81
  47. data/test/og/tc_join.rb +67 -0
  48. data/test/og/tc_polymorphic.rb +49 -49
  49. data/test/og/tc_relation.rb +57 -30
  50. data/test/og/tc_select.rb +49 -0
  51. data/test/og/tc_store.rb +345 -337
  52. data/test/og/tc_types.rb +11 -11
  53. metadata +11 -18
@@ -6,736 +6,888 @@ module Og
6
6
 
7
7
  module SqlUtils
8
8
 
9
- # Escape an SQL string
10
-
11
- def escape(str)
12
- return nil unless str
13
- return str.gsub(/'/, "''")
14
- end
15
-
16
- # Convert a ruby time to an sql timestamp.
17
- #--
18
- # TODO: Optimize this
19
- #++
20
-
21
- def timestamp(time = Time.now)
22
- return nil unless time
23
- return time.strftime("%Y-%m-%d %H:%M:%S")
24
- end
25
-
26
- # Output YYY-mm-dd
27
- #--
28
- # TODO: Optimize this.
29
- #++
30
-
31
- def date(date)
32
- return nil unless date
33
- return "#{date.year}-#{date.month}-#{date.mday}"
34
- end
35
-
36
- #--
37
- # TODO: implement me!
38
- #++
39
-
40
- def blob(val)
41
- val
42
- end
43
-
44
- # Parse an integer.
45
-
46
- def parse_int(int)
47
- int = int.to_i if int
48
- int
49
- end
50
-
51
- # Parse a float.
52
-
53
- def parse_float(fl)
54
- fl = fl.to_f if fl
55
- fl
56
- end
57
-
58
- # Parse sql datetime
59
- #--
60
- # TODO: Optimize this.
61
- #++
62
-
63
- def parse_timestamp(str)
64
- return nil unless str
65
- return Time.parse(str)
66
- end
67
-
68
- # Input YYYY-mm-dd
69
- #--
70
- # TODO: Optimize this.
71
- #++
72
-
73
- def parse_date(str)
74
- return nil unless str
75
- return Date.strptime(str)
76
- end
77
-
78
- #--
79
- # TODO: implement me!!
80
- #++
81
-
82
- def parse_blob(val)
83
- val
84
- end
85
-
86
- # Escape the various Ruby types.
87
-
88
- def quote(val)
89
- case val
90
- when Fixnum, Integer, Float
91
- val ? val.to_s : 'NULL'
92
- when String
93
- val ? "'#{escape(val)}'" : 'NULL'
94
- when Time
95
- val ? "'#{timestamp(val)}'" : 'NULL'
96
- when Date
97
- val ? "'#{date(val)}'" : 'NULL'
98
- when TrueClass
99
- val ? "'t'" : 'NULL'
100
- else
101
- # gmosx: keep the '' for nil symbols.
102
- val ? escape(val.to_yaml) : ''
103
- end
104
- end
105
-
106
- def table(klass)
107
- "#{Og.table_prefix}#{klass.to_s.gsub(/::/, "_").downcase}"
108
- end
109
-
110
- def join_table(class1, class2, postfix = nil)
111
- if class1.to_s <= class2.to_s
112
- return "j#{table(class1)}#{table(class2)}#{postfix}", 1, 2
113
- else
114
- return "j#{table(class2)}#{table(class1)}#{postfix}", 2, 1
115
- end
116
- end
9
+ # Escape an SQL string
10
+
11
+ def escape(str)
12
+ return nil unless str
13
+ return str.gsub(/'/, "''")
14
+ end
15
+
16
+ # Convert a ruby time to an sql timestamp.
17
+ #--
18
+ # TODO: Optimize this
19
+ #++
20
+
21
+ def timestamp(time = Time.now)
22
+ return nil unless time
23
+ return time.strftime("%Y-%m-%d %H:%M:%S")
24
+ end
25
+
26
+ # Output YYY-mm-dd
27
+ #--
28
+ # TODO: Optimize this.
29
+ #++
30
+
31
+ def date(date)
32
+ return nil unless date
33
+ return "#{date.year}-#{date.month}-#{date.mday}"
34
+ end
35
+
36
+ #--
37
+ # TODO: implement me!
38
+ #++
39
+
40
+ def blob(val)
41
+ val
42
+ end
43
+
44
+ # Parse an integer.
45
+
46
+ def parse_int(int)
47
+ int = int.to_i if int
48
+ int
49
+ end
50
+
51
+ # Parse a float.
52
+
53
+ def parse_float(fl)
54
+ fl = fl.to_f if fl
55
+ fl
56
+ end
57
+
58
+ # Parse sql datetime
59
+ #--
60
+ # TODO: Optimize this.
61
+ #++
62
+
63
+ def parse_timestamp(str)
64
+ return nil unless str
65
+ return Time.parse(str)
66
+ end
67
+
68
+ # Input YYYY-mm-dd
69
+ #--
70
+ # TODO: Optimize this.
71
+ #++
72
+
73
+ def parse_date(str)
74
+ return nil unless str
75
+ return Date.strptime(str)
76
+ end
77
+
78
+ #--
79
+ # TODO: implement me!!
80
+ #++
81
+
82
+ def parse_blob(val)
83
+ val
84
+ end
85
+
86
+ # Escape the various Ruby types.
87
+
88
+ def quote(val)
89
+ case val
90
+ when Fixnum, Integer, Float
91
+ val ? val.to_s : 'NULL'
92
+ when String
93
+ val ? "'#{escape(val)}'" : 'NULL'
94
+ when Time
95
+ val ? "'#{timestamp(val)}'" : 'NULL'
96
+ when Date
97
+ val ? "'#{date(val)}'" : 'NULL'
98
+ when TrueClass
99
+ val ? "'t'" : 'NULL'
100
+ else
101
+ # gmosx: keep the '' for nil symbols.
102
+ val ? escape(val.to_yaml) : ''
103
+ end
104
+ end
105
+
106
+ # Apply table name conventions to a class name.
107
+
108
+ def tableize(klass)
109
+ "#{klass.to_s.gsub(/::/, "_").downcase}"
110
+ end
111
+
112
+ def table(klass)
113
+ "#{Og.table_prefix}#{tableize(klass)}"
114
+ end
115
+
116
+ def join_object_ordering(obj1, obj2)
117
+ if obj1.class.to_s <= obj2.class.to_s
118
+ return obj1, obj2
119
+ else
120
+ return obj2, obj1, true
121
+ end
122
+ end
123
+
124
+ def join_class_ordering(class1, class2)
125
+ if class1.to_s <= class2.to_s
126
+ return class1, class2
127
+ else
128
+ return class2, class1, true
129
+ end
130
+ end
131
+
132
+ def build_join_name(class1, class2, postfix = nil)
133
+ # Don't reorder arguments, as this is used in places that
134
+ # have already determined the order they want.
135
+ "#{Og.table_prefix}j_#{tableize(class1)}_#{tableize(class2)}#{postfix}"
136
+ end
137
+
138
+ def join_table(class1, class2, postfix = nil)
139
+ first, second = join_class_ordering(class1, class2)
140
+ build_join_name(first, second, postfix)
141
+ end
142
+
143
+ def join_table_index(key)
144
+ "#{key}_idx"
145
+ end
146
+
147
+ def join_table_key(klass)
148
+ "#{klass.to_s.split('::').last.downcase}_oid"
149
+ end
150
+ def join_table_keys(class1, class2)
151
+ if class1 == class2
152
+ # Fix for the self-join case.
153
+ return join_table_key(class1), "#{join_table_key(class2)}2"
154
+ else
155
+ return join_table_key(class1), join_table_key(class2)
156
+ end
157
+ end
158
+
159
+ def join_table_info(owner_class, target_class, postfix = nil)
160
+ owner_key, target_key = join_table_keys(owner_class, target_class)
161
+ first, second, changed = join_class_ordering(owner_class, target_class)
162
+
163
+ if changed
164
+ first_key, second_key = target_key, owner_key
165
+ else
166
+ first_key, second_key = owner_key, target_key
167
+ end
168
+
169
+ return {
170
+ :table => join_table(owner_class, target_class, postfix),
171
+ :owner_key => owner_key,
172
+ :target_key => target_key,
173
+ :first_table => table(first),
174
+ :first_key => first_key,
175
+ :first_index => join_table_index(first_key),
176
+ :second_table => table(second),
177
+ :second_key => second_key,
178
+ :second_index => join_table_index(second_key)
179
+ }
180
+ end
181
+
182
+ # Subclasses can override this if they need a different
183
+ # syntax.
184
+
185
+ def create_join_table_sql(join_table_info, suffix = 'NOT NULL', key_type = 'integer')
186
+ join_table = join_table_info[:table]
187
+ first_index = join_table_info[:first_index]
188
+ first_key = join_table_info[:first_key]
189
+ second_key = join_table_info[:second_key]
190
+ second_index = join_table_info[:second_index]
191
+
192
+ sql = []
193
+
194
+ sql << %{
195
+ CREATE TABLE #{join_table} (
196
+ #{first_key} integer NOT NULL,
197
+ #{second_key} integer NOT NULL,
198
+ PRIMARY KEY(#{first_key}, #{second_key})
199
+ )
200
+ }
201
+
202
+ # gmosx: not that useful?
203
+ # sql << "CREATE INDEX #{first_index} ON #{join_table} (#{first_key})"
204
+ # sql << "CREATE INDEX #{second_index} ON #{join_table} (#{second_key})"
205
+
206
+ return sql
207
+ end
208
+
117
209
  end
118
210
 
119
- # A Store that persists objects into a PostgreSQL database.
211
+ # An abstract SQL powered store.
120
212
 
121
213
  class SqlStore < Store
122
- extend SqlUtils
123
- include SqlUtils
124
-
125
- # The connection to the backend SQL RDBMS.
126
-
127
- attr_accessor :conn
128
-
129
- def initialize(options)
130
- super
131
-
132
- # The default Ruby <-> SQL type mappings, should be valid for most
133
- # RDBM systems.
134
-
135
- @typemap = {
136
- Integer => 'integer',
137
- Fixnum => 'integer',
138
- Float => 'float',
139
- String => 'text',
140
- Time => 'timestamp',
141
- Date => 'date',
142
- TrueClass => 'boolean',
143
- Object => 'text',
144
- Array => 'text',
145
- Hash => 'text'
146
- }
147
- end
148
-
149
- #--
150
- # FIXME: not working.
151
- #++
152
-
153
- def enable_logging
154
- require 'glue/aspects'
155
- klass = self.class
156
- klass.send :include, Glue::Aspects
157
- klass.pre "Logger.info sql", :on => [:exec, :query]
158
- Glue::Aspects.wrap(klass, [:exec, :query])
159
- end
160
-
161
- # Enchants a class.
162
-
163
- def enchant(klass, manager)
164
-
165
- # setup the table where this class is mapped.
166
-
167
- if sclass = klass.metadata.superclass
168
- klass.const_set 'OGTABLE', table(sclass.first)
169
- else
170
- klass.const_set 'OGTABLE', table(klass)
171
- end
172
-
173
- klass.module_eval 'def self.table; OGTABLE; end'
174
-
175
- # precompile a class specific allocate method. If this
176
- # is an STI parent classes it reads the class from the
177
- # resultset.
178
-
179
- if klass.metadata.subclasses
180
- klass.module_eval %{
181
- def self.og_allocate(res)
182
- Object.constant(res[0]).allocate
183
- end
184
- }
185
- else
186
- klass.module_eval %{
187
- def self.og_allocate(res)
188
- self.allocate
189
- end
190
- }
191
- end
192
-
193
- super
194
-
195
- unless klass.polymorphic_parent?
196
- # create the table if needed.
197
-
198
- create_table(klass) if Og.create_schema
199
-
200
- # precompile class specific lifecycle methods.
201
-
202
- eval_og_insert(klass)
203
- eval_og_update(klass)
204
- eval_og_read(klass)
205
- eval_og_delete(klass)
206
- end
207
- end
208
-
209
- # :section: Lifecycle methods.
210
-
211
- # Loads an object from the store using the primary key.
212
-
213
- def load(pk, klass)
214
- res = query "SELECT * FROM #{klass::OGTABLE} WHERE #{klass.pk_symbol}=#{pk}"
215
- read_one(res, klass)
216
- end
217
- alias_method :exist?, :load
218
-
219
- # Reloads an object from the store.
220
-
221
- def reload(obj, pk)
222
- raise 'Cannot reload unmanaged object' unless obj.saved?
223
- res = query "SELECT * FROM #{obj.class.table} WHERE #{obj.class.pk_symbol}=#{pk}"
224
- obj.og_read(res.next, 0)
225
- ensure
226
- res.close if res
227
- end
228
-
229
- # If a properties collection is provided, only updates the
230
- # selected properties. Pass the required properties as symbols
231
- # or strings.
232
-
233
- def update(obj, options = nil)
234
- if options and properties = options[:only]
235
- if properties.is_a?(Array)
236
- set = []
237
- for p in properties
238
- set << "#{p}=#{quote(obj.send(p))}"
239
- end
240
- set = set.join(',')
241
- else
242
- set = "#{properties}=#{quote(obj.send(properties))}"
243
- end
244
- sql = "UPDATE #{obj.class.table} SET #{set} WHERE #{obj.class.pk_symbol}=#{obj.pk}"
245
- sql << " AND #{options[:condition]}" if options[:condition]
246
- sql_update(sql)
247
- else
248
- obj.og_update(self, options)
249
- end
250
- end
251
-
252
- # Update selected properties of an object or class of
253
- # objects.
254
-
255
- def update_properties(target, set, options = nil)
256
- set = set.gsub(/@/, '')
257
-
258
- if target.is_a?(Class)
259
- sql = "UPDATE #{target.table} SET #{set} "
260
- sql << " WHERE #{options[:condition]}" if options and options[:condition]
261
- sql_update(sql)
262
- else
263
- sql = "UPDATE #{target.class.table} SET #{set} WHERE #{target.class.pk_symbol}=#{target.pk}"
264
- sql << " AND #{options[:condition]}" if options and options[:condition]
265
- sql_update(sql)
266
- end
267
- end
268
- alias_method :pupdate, :update_properties
269
- alias_method :update_property, :update_properties
270
-
271
- # Find a collection of objects.
272
- #
273
- # === Examples
274
- #
275
- # User.find(:condition => 'age > 15', :order => 'score ASC', :offet => 10, :limit =>10)
276
- # Comment.find(:include => :entry)
277
-
278
- def find(options)
279
- klass = options[:class]
280
- sql = resolve_options(klass, options)
281
- read_all(query(sql), klass, options[:include])
282
- end
283
-
284
- # Find one object.
285
-
286
- def find_one(options)
287
- klass = options[:class]
214
+ extend SqlUtils
215
+ include SqlUtils
216
+
217
+ # The connection to the backend SQL RDBMS.
218
+
219
+ attr_accessor :conn
220
+
221
+ def initialize(options)
222
+ super
223
+
224
+ # The default Ruby <-> SQL type mappings, should be valid for most
225
+ # RDBM systems.
226
+
227
+ @typemap = {
228
+ Integer => 'integer',
229
+ Fixnum => 'integer',
230
+ Float => 'float',
231
+ String => 'text',
232
+ Time => 'timestamp',
233
+ Date => 'date',
234
+ TrueClass => 'boolean',
235
+ Object => 'text',
236
+ Array => 'text',
237
+ Hash => 'text',
238
+ Og::Blob => 'bytea' # psql
239
+ }
240
+ end
241
+
242
+ #--
243
+ # FIXME: not working.
244
+ #++
245
+
246
+ def enable_logging
247
+ require 'glue/aspects'
248
+ klass = self.class
249
+ klass.send :include, Glue::Aspects
250
+ klass.pre "Logger.info sql", :on => [:exec, :query]
251
+ Glue::Aspects.wrap(klass, [:exec, :query])
252
+ end
253
+
254
+ # Enchants a class.
255
+
256
+ def enchant(klass, manager)
257
+
258
+ # setup the table where this class is mapped.
259
+
260
+ if sclass = klass.metadata.superclass
261
+ klass.const_set 'OGTABLE', table(sclass.first)
262
+ else
263
+ klass.const_set 'OGTABLE', table(klass)
264
+ end
265
+
266
+ klass.module_eval 'def self.table; OGTABLE; end'
267
+
268
+ # precompile a class specific allocate method. If this
269
+ # is an STI parent classes it reads the class from the
270
+ # resultset.
271
+
272
+ if klass.metadata.subclasses
273
+ klass.module_eval %{
274
+ def self.og_allocate(res)
275
+ Object.constant(res[0]).allocate
276
+ end
277
+ }
278
+ else
279
+ klass.module_eval %{
280
+ def self.og_allocate(res)
281
+ self.allocate
282
+ end
283
+ }
284
+ end
285
+
286
+ super
287
+
288
+ unless klass.polymorphic_parent?
289
+ # create the table if needed.
290
+
291
+ eval_og_create_schema(klass)
292
+ # create_table(klass) if Og.create_schema
293
+ klass.allocate.og_create_schema(self)
294
+
295
+ # precompile class specific lifecycle methods.
296
+
297
+ eval_og_insert(klass)
298
+ eval_og_update(klass)
299
+ eval_og_read(klass)
300
+ eval_og_delete(klass)
301
+ end
302
+ end
303
+
304
+ # :section: Lifecycle methods.
305
+
306
+ # Loads an object from the store using the primary key.
307
+
308
+ def load(pk, klass)
309
+ res = query "SELECT * FROM #{klass::OGTABLE} WHERE #{klass.pk_symbol}=#{pk}"
310
+ read_one(res, klass)
311
+ end
312
+ alias_method :exist?, :load
313
+
314
+ # Reloads an object from the store.
315
+
316
+ def reload(obj, pk)
317
+ raise 'Cannot reload unmanaged object' unless obj.saved?
318
+ res = query "SELECT * FROM #{obj.class.table} WHERE #{obj.class.pk_symbol}=#{pk}"
319
+ obj.og_read(res.next, 0)
320
+ ensure
321
+ res.close if res
322
+ end
323
+
324
+ # If a properties collection is provided, only updates the
325
+ # selected properties. Pass the required properties as symbols
326
+ # or strings.
327
+
328
+ def update(obj, options = nil)
329
+ if options and properties = options[:only]
330
+ if properties.is_a?(Array)
331
+ set = []
332
+ for p in properties
333
+ set << "#{p}=#{quote(obj.send(p))}"
334
+ end
335
+ set = set.join(',')
336
+ else
337
+ set = "#{properties}=#{quote(obj.send(properties))}"
338
+ end
339
+ sql = "UPDATE #{obj.class.table} SET #{set} WHERE #{obj.class.pk_symbol}=#{obj.pk}"
340
+ sql << " AND #{options[:condition]}" if options[:condition]
341
+ sql_update(sql)
342
+ else
343
+ obj.og_update(self, options)
344
+ end
345
+ end
346
+
347
+ # Update selected properties of an object or class of
348
+ # objects.
349
+
350
+ def update_properties(target, set, options = nil)
351
+ set = set.gsub(/@/, '')
352
+
353
+ if target.is_a?(Class)
354
+ sql = "UPDATE #{target.table} SET #{set} "
355
+ sql << " WHERE #{options[:condition]}" if options and options[:condition]
356
+ sql_update(sql)
357
+ else
358
+ sql = "UPDATE #{target.class.table} SET #{set} WHERE #{target.class.pk_symbol}=#{target.pk}"
359
+ sql << " AND #{options[:condition]}" if options and options[:condition]
360
+ sql_update(sql)
361
+ end
362
+ end
363
+ alias_method :pupdate, :update_properties
364
+ alias_method :update_property, :update_properties
365
+
366
+ # Find a collection of objects.
367
+ #
368
+ # === Examples
369
+ #
370
+ # User.find(:condition => 'age > 15', :order => 'score ASC', :offet => 10, :limit =>10)
371
+ # Comment.find(:include => :entry)
372
+
373
+ def find(options)
374
+ klass = options[:class]
375
+ sql = resolve_options(klass, options)
376
+ read_all(query(sql), klass, options)
377
+ end
378
+
379
+ # Find one object.
380
+
381
+ def find_one(options)
382
+ klass = options[:class]
288
383
  # gmosx, THINK: should not set this by default.
289
- # options[:limit] ||= 1
290
- sql = resolve_options(klass, options)
291
- read_one(query(sql), klass, options[:include])
292
- end
293
-
294
- # Perform a custom sql query and deserialize the
295
- # results.
296
-
297
- def select(sql, klass)
298
- sql = "SELECT * FROM #{klass.table} " + sql unless sql =~ /SELECT/
299
- read_all(query(sql), klass)
300
- end
301
- alias_method :find_by_sql, :select
302
-
303
- # Specialized one result version of select.
304
-
305
- def select_one(sql, klass)
306
- sql = "SELECT * FROM #{klass.table} " + sql unless sql =~ /SELECT/
307
- read_one(query(sql), klass)
308
- end
309
-
310
- def count(options)
311
- if options.is_a?(String)
312
- sql = options
313
- else
314
- sql = "SELECT COUNT(*) FROM #{options[:class]::OGTABLE}"
315
- if condition = options[:condition]
316
- sql << " WHERE #{condition}"
317
- end
318
- end
319
-
320
- query(sql).first_value.to_i
321
- end
322
-
323
- # Relate two objects through an intermediate join table.
324
- # Typically used in joins_many and many_to_many relations.
325
-
326
- def join(obj1, obj2, table)
327
- if obj1.class.to_s > obj2.class.to_s
328
- obj1, obj2 = obj2, obj1
329
- end
330
-
331
- exec "INSERT INTO #{table} (key1, key2) VALUES (#{obj1.pk}, #{obj2.pk})"
332
- end
333
-
334
- # Unrelate two objects be removing their relation from the
335
- # join table.
336
-
337
- def unjoin(obj1, obj2, table)
338
- if obj1.class.to_s > obj2.class.to_s
339
- obj1, obj2 = obj2, obj1
340
- end
341
-
342
- exec "DELETE FROM #{table} WHERE key1=#{obj1.pk} AND key2=#{obj2.pk}"
343
- end
344
-
345
- # :section: Transaction methods.
346
-
347
- # Start a new transaction.
348
-
349
- def start
350
- exec('START TRANSACTION') if @transaction_nesting < 1
351
- @transaction_nesting += 1
352
- end
353
-
354
- # Commit a transaction.
355
-
356
- def commit
357
- @transaction_nesting -= 1
358
- exec('COMMIT') if @transaction_nesting < 1
359
- end
360
-
361
- # Rollback a transaction.
362
-
363
- def rollback
364
- @transaction_nesting -= 1
365
- exec('ROLLBACK') if @transaction_nesting < 1
366
- end
367
-
368
- # :section: Low level methods.
369
-
370
- # Encapsulates a low level update method.
371
-
372
- def sql_update(sql)
373
- exec(sql)
374
- # return affected rows.
375
- end
384
+ # options[:limit] ||= 1
385
+ sql = resolve_options(klass, options)
386
+ read_one(query(sql), klass, options)
387
+ end
388
+
389
+ # Perform a custom sql query and deserialize the
390
+ # results.
391
+
392
+ def select(sql, klass)
393
+ sql = "SELECT * FROM #{klass.table} " + sql unless sql =~ /SELECT/
394
+ read_all(query(sql), klass)
395
+ end
396
+ alias_method :find_by_sql, :select
397
+
398
+ # Specialized one result version of select.
399
+
400
+ def select_one(sql, klass)
401
+ sql = "SELECT * FROM #{klass.table} " + sql unless sql =~ /SELECT/
402
+ read_one(query(sql), klass)
403
+ end
404
+ alias_method :find_by_sql_one, :select_one
405
+
406
+ # Perform an aggregation over query results.
407
+
408
+ def aggregate(options)
409
+ if options.is_a?(String)
410
+ sql = options
411
+ else
412
+ aggregate = options[:aggregate] || 'COUNT(*)'
413
+ sql = "SELECT #{aggregate} FROM #{options[:class].table}"
414
+ if condition = options[:condition]
415
+ sql << " WHERE #{condition}"
416
+ end
417
+ end
418
+
419
+ query(sql).first_value.to_i
420
+ end
421
+ alias_method :count, :aggregate
422
+
423
+ # Relate two objects through an intermediate join table.
424
+ # Typically used in joins_many and many_to_many relations.
425
+
426
+ def join(obj1, obj2, table, options = nil)
427
+ first, second = join_object_ordering(obj1, obj2)
428
+ first_key, second_key = join_table_keys(obj1.class, obj2.class)
429
+ if options
430
+ exec "INSERT INTO #{table} (#{first_key},#{second_key}, #{options.keys.join(',')}) VALUES (#{first.pk},#{second.pk}, #{options.values.map { |v| quote(v) }.join(',')})"
431
+ else
432
+ exec "INSERT INTO #{table} (#{first_key},#{second_key}) VALUES (#{first.pk}, #{second.pk})"
433
+ end
434
+ end
435
+
436
+ # Unrelate two objects be removing their relation from the
437
+ # join table.
438
+
439
+ def unjoin(obj1, obj2, table)
440
+ first, second = join_object_ordering(obj1, obj2)
441
+ first_key, second_key = join_table_keys(obj1, obj2, table)
442
+ exec "DELETE FROM #{table} WHERE #{first_key}=#{first.pk} AND #{second_key}=#{second.pk}"
443
+ end
444
+
445
+ # :section: Transaction methods.
446
+
447
+ # Start a new transaction.
448
+
449
+ def start
450
+ exec('START TRANSACTION') if @transaction_nesting < 1
451
+ @transaction_nesting += 1
452
+ end
453
+
454
+ # Commit a transaction.
455
+
456
+ def commit
457
+ @transaction_nesting -= 1
458
+ exec('COMMIT') if @transaction_nesting < 1
459
+ end
460
+
461
+ # Rollback a transaction.
462
+
463
+ def rollback
464
+ @transaction_nesting -= 1
465
+ exec('ROLLBACK') if @transaction_nesting < 1
466
+ end
467
+
468
+ # :section: Low level methods.
469
+
470
+ # Encapsulates a low level update method.
471
+
472
+ def sql_update(sql)
473
+ exec(sql)
474
+ # return affected rows.
475
+ end
376
476
 
377
477
  private
378
478
 
379
- def create_table(klass)
380
- raise 'Not implemented'
381
- end
382
-
383
- def drop_table(klass)
384
- exec "DROP TABLE #{klass.table}"
385
- end
386
-
387
- # Create the fields that correpsond to the klass properties.
388
- # The generated fields array is used in create_table.
389
- # If the property has an :sql metadata this overrides the
390
- # default mapping. If the property has an :extra_sql metadata
391
- # the extra sql is appended after the default mapping.
392
-
393
- def fields_for_class(klass)
394
- fields = []
395
- properties = klass.properties
396
-
397
- if subclasses = klass.metadata.subclasses
398
- # This class as a superclass in a single table inheritance
399
- # chain. So inject a special class ogtype field that
400
- # holds the class name.
401
- fields << "ogtype VARCHAR(30)"
402
-
403
- for subclass in subclasses
404
- properties.concat(subclass.properties)
405
- end
406
-
407
- properties.uniq!
408
- end
409
-
410
- properties.each do |p|
411
- klass.index(p.symbol) if p.meta[:index]
412
-
413
- field = p.symbol.to_s
414
-
415
- if p.meta and p.meta[:sql]
416
- field << " #{p.meta[:sql]}"
417
- else
418
- field << " #{type_for_class(p.klass)}"
419
-
420
- if p.meta
421
- field << " UNIQUE" if p.meta[:unique]
422
-
423
- if default = p.meta[:default]
424
- field << " DEFAULT #{default.inspect} NOT NULL"
425
- end
426
-
427
- if extra_sql = p.meta[:extra_sql]
428
- field << " #{extra_sql}"
429
- end
430
- end
431
- end
432
-
433
- fields << field
434
- end
435
-
436
- return fields
437
- end
438
-
439
- def type_for_class(klass)
440
- @typemap[klass]
441
- end
442
-
443
- # Return an sql string evaluator for the property.
444
- # No need to optimize this, used only to precalculate code.
445
- # YAML is used to store general Ruby objects to be more
446
- # portable.
447
- #--
448
- # FIXME: add extra handling for float.
449
- #++
450
-
451
- def write_prop(p)
452
- if p.klass.ancestors.include?(Integer)
453
- return "#\{@#{p.symbol} || 'NULL'\}"
454
- elsif p.klass.ancestors.include?(Float)
455
- return "#\{@#{p.symbol} || 'NULL'\}"
456
- elsif p.klass.ancestors.include?(String)
457
- return %|#\{@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol})\}'" : 'NULL'\}|
458
- elsif p.klass.ancestors.include?(Time)
459
- return %|#\{@#{p.symbol} ? "'#\{#{self.class}.timestamp(@#{p.symbol})\}'" : 'NULL'\}|
460
- elsif p.klass.ancestors.include?(Date)
461
- return %|#\{@#{p.symbol} ? "'#\{#{self.class}.date(@#{p.symbol})\}'" : 'NULL'\}|
462
- elsif p.klass.ancestors.include?(TrueClass)
463
- return "#\{@#{p.symbol} ? \"'t'\" : 'NULL' \}"
479
+ def create_table(klass)
480
+ raise 'Not implemented'
481
+ end
482
+
483
+ def drop_table(klass)
484
+ exec "DROP TABLE #{klass.table}"
485
+ end
486
+
487
+ # Create the fields that correpsond to the klass properties.
488
+ # The generated fields array is used in create_table.
489
+ # If the property has an :sql metadata this overrides the
490
+ # default mapping. If the property has an :extra_sql metadata
491
+ # the extra sql is appended after the default mapping.
492
+
493
+ def fields_for_class(klass)
494
+ fields = []
495
+ properties = klass.properties
496
+
497
+ if subclasses = klass.metadata.subclasses
498
+ # This class as a superclass in a single table inheritance
499
+ # chain. So inject a special class ogtype field that
500
+ # holds the class name.
501
+ fields << "ogtype VARCHAR(30)"
502
+
503
+ for subclass in subclasses
504
+ properties.concat(subclass.properties)
505
+ end
506
+
507
+ properties.uniq!
508
+ end
509
+
510
+ properties.each do |p|
511
+ klass.index(p.symbol) if p.meta[:index]
512
+
513
+ field = p.symbol.to_s
514
+
515
+ if p.meta and p.meta[:sql]
516
+ field << " #{p.meta[:sql]}"
517
+ else
518
+ field << " #{type_for_class(p.klass)}"
519
+
520
+ if p.meta
521
+ field << " UNIQUE" if p.meta[:unique]
522
+
523
+ if default = p.meta[:default]
524
+ field << " DEFAULT #{default.inspect} NOT NULL"
525
+ end
526
+
527
+ if extra_sql = p.meta[:extra_sql]
528
+ field << " #{extra_sql}"
529
+ end
530
+ end
531
+ end
532
+
533
+ fields << field
534
+ end
535
+
536
+ return fields
537
+ end
538
+
539
+ def type_for_class(klass)
540
+ @typemap[klass]
541
+ end
542
+
543
+ # Return an sql string evaluator for the property.
544
+ # No need to optimize this, used only to precalculate code.
545
+ # YAML is used to store general Ruby objects to be more
546
+ # portable.
547
+ #--
548
+ # FIXME: add extra handling for float.
549
+ #++
550
+
551
+ def write_prop(p)
552
+ if p.klass.ancestors.include?(Integer)
553
+ return "#\{@#{p.symbol} || 'NULL'\}"
554
+ elsif p.klass.ancestors.include?(Float)
555
+ return "#\{@#{p.symbol} || 'NULL'\}"
556
+ elsif p.klass.ancestors.include?(String)
557
+ return %|#\{@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol})\}'" : 'NULL'\}|
558
+ elsif p.klass.ancestors.include?(Time)
559
+ return %|#\{@#{p.symbol} ? "'#\{#{self.class}.timestamp(@#{p.symbol})\}'" : 'NULL'\}|
560
+ elsif p.klass.ancestors.include?(Date)
561
+ return %|#\{@#{p.symbol} ? "'#\{#{self.class}.date(@#{p.symbol})\}'" : 'NULL'\}|
562
+ elsif p.klass.ancestors.include?(TrueClass)
563
+ return "#\{@#{p.symbol} ? \"'t'\" : 'NULL' \}"
464
564
  elsif p.klass.ancestors.include?(Og::Blob)
465
- return %|#\{@#{p.symbol} ? "'#\{#{self.class}.escape(#{self.class}.blob(@#{p.symbol}))\}'" : 'NULL'\}|
466
- else
467
- # gmosx: keep the '' for nil symbols.
468
- return %|#\{@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol}.to_yaml)\}'" : "''"\}|
469
- end
470
- end
471
-
472
- # Return an evaluator for reading the property.
473
- # No need to optimize this, used only to precalculate code.
474
-
475
- def read_prop(p, col)
476
- if p.klass.ancestors.include?(Integer)
477
- return "#{self.class}.parse_int(res[#{col} + offset])"
478
- elsif p.klass.ancestors.include?(Float)
479
- return "#{self.class}.parse_float(res[#{col} + offset])"
480
- elsif p.klass.ancestors.include?(String)
481
- return "res[#{col} + offset]"
482
- elsif p.klass.ancestors.include?(Time)
483
- return "#{self.class}.parse_timestamp(res[#{col} + offset])"
484
- elsif p.klass.ancestors.include?(Date)
485
- return "#{self.class}.parse_date(res[#{col} + offset])"
486
- elsif p.klass.ancestors.include?(TrueClass)
487
- return "('0' != res[#{col} + offset])"
565
+ return %|#\{@#{p.symbol} ? "'#\{#{self.class}.escape(#{self.class}.blob(@#{p.symbol}))\}'" : 'NULL'\}|
566
+ else
567
+ # gmosx: keep the '' for nil symbols.
568
+ return %|#\{@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol}.to_yaml)\}'" : "''"\}|
569
+ end
570
+ end
571
+
572
+ # Return an evaluator for reading the property.
573
+ # No need to optimize this, used only to precalculate code.
574
+
575
+ def read_prop(p, col)
576
+ if p.klass.ancestors.include?(Integer)
577
+ return "#{self.class}.parse_int(res[#{col} + offset])"
578
+ elsif p.klass.ancestors.include?(Float)
579
+ return "#{self.class}.parse_float(res[#{col} + offset])"
580
+ elsif p.klass.ancestors.include?(String)
581
+ return "res[#{col} + offset]"
582
+ elsif p.klass.ancestors.include?(Time)
583
+ return "#{self.class}.parse_timestamp(res[#{col} + offset])"
584
+ elsif p.klass.ancestors.include?(Date)
585
+ return "#{self.class}.parse_date(res[#{col} + offset])"
586
+ elsif p.klass.ancestors.include?(TrueClass)
587
+ return "('0' != res[#{col} + offset])"
488
588
  elsif p.klass.ancestors.include?(Og::Blob)
489
- return "#{self.class}.parse_blob(res[#{col} + offset])"
490
- else
491
- return "YAML::load(res[#{col} + offset])"
492
- end
493
- end
494
-
495
- # :section: Lifecycle method compilers.
496
-
497
- # Compile the og_insert method for the class.
498
-
499
- def eval_og_insert(klass)
500
- pk = klass.pk_symbol
501
- props = klass.properties
502
- values = props.collect { |p| write_prop(p) }.join(',')
503
-
504
- if klass.metadata.superclass or klass.metadata.subclasses
505
- props << Property.new(:ogtype, String)
506
- values << ", '#{klass}'"
507
- end
508
-
509
- sql = "INSERT INTO #{klass.table} (#{props.collect {|p| p.symbol.to_s}.join(',')}) VALUES (#{values})"
510
-
511
- klass.module_eval %{
512
- def og_insert(store)
513
- #{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
514
- store.exec "#{sql}"
515
- #{Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
516
- end
517
- }
518
- end
519
-
520
- # Compile the og_update method for the class.
521
-
522
- def eval_og_update(klass)
523
- pk = klass.pk_symbol
524
- props = klass.properties.reject { |p| pk == p.symbol }
525
-
526
- updates = props.collect { |p|
527
- "#{p.symbol}=#{write_prop(p)}"
528
- }
529
-
530
- sql = "UPDATE #{klass::OGTABLE} SET #{updates.join(', ')} WHERE #{pk}=#\{@#{pk}\}"
531
-
532
- klass.module_eval %{
533
- def og_update(store, options = nil)
534
- #{Aspects.gen_advice_code(:og_update, klass.advices, :pre) if klass.respond_to?(:advices)}
535
- sql = "#{sql}"
536
- sql << " AND \#{options[:condition]}" if options and options[:condition]
537
- changed = store.sql_update(sql)
538
- #{Aspects.gen_advice_code(:og_update, klass.advices, :post) if klass.respond_to?(:advices)}
539
- return changed
540
- end
541
- }
542
- end
543
-
544
- # Compile the og_read method for the class. This method is
545
- # used to read (deserialize) the given class from the store.
546
- # In order to allow for changing field/attribute orders a
547
- # field mapping hash is used.
548
-
549
- def eval_og_read(klass)
550
- code = []
551
- props = klass.properties
552
- field_map = create_field_map(klass)
553
-
554
- props.each do |p|
555
- if col = field_map[p.symbol]
556
- code << "@#{p.symbol} = #{read_prop(p, col)}"
557
- end
558
- end
559
-
560
- code = code.join('; ')
561
-
562
- klass.module_eval %{
563
- def og_read(res, row = 0, offset = 0)
564
- #{Aspects.gen_advice_code(:og_read, klass.advices, :pre) if klass.respond_to?(:advices)}
565
- #{code}
566
- #{Aspects.gen_advice_code(:og_read, klass.advices, :post) if klass.respond_to?(:advices)}
567
- end
568
- }
569
- end
570
-
571
- #--
572
- # FIXME: is pk needed as parameter?
573
- #++
574
-
575
- def eval_og_delete(klass)
576
- klass.module_eval %{
577
- def og_delete(store, pk, cascade = true)
578
- #{Aspects.gen_advice_code(:og_delete, klass.advices, :pre) if klass.respond_to?(:advices)}
579
- pk ||= @#{klass.pk_symbol}
580
- transaction do |tx|
581
- tx.exec "DELETE FROM #{klass.table} WHERE #{klass.pk_symbol}=\#{pk}"
582
- if cascade and #{klass}.metadata[:descendants]
583
- #{klass}.metadata[:descendants].each do |dclass, foreign_key|
584
- tx.exec "DELETE FROM \#{dclass::OGTABLE} WHERE \#{foreign_key}=\#{pk}"
585
- end
586
- end
587
- end
588
- #{Aspects.gen_advice_code(:og_delete, klass.advices, :post) if klass.respond_to?(:advices)}
589
- end
590
- }
591
- end
592
-
593
- # :section: Misc methods.
594
-
595
- def handle_sql_exception(ex, sql = nil)
596
- Logger.error "DB error #{ex}, [#{sql}]"
597
- Logger.error ex.backtrace.join("\n")
598
- raise StoreException.new(ex, sql) if Og.raise_store_exceptions
599
-
600
- # FIXME: should return :error or something.
601
- return nil
602
- end
603
-
604
- def resolve_options(klass, options)
605
- if sql = options[:sql]
606
- sql = "SELECT * FROM #{klass.table} " + sql unless sql =~ /SELECT/
607
- return sql
608
- end
609
-
610
- tables = [klass::OGTABLE]
611
-
612
- if included = options[:include]
613
- join_conditions = []
614
-
615
- for name in [included].flatten
616
- if rel = klass.relation(name)
617
- target_table = rel[:target_class]::OGTABLE
618
- tables << target_table
619
- join_conditions << "#{klass::OGTABLE}.#{rel[:foreign_key]}=#{target_table}.#{rel[:target_pk]}"
620
- else
621
- raise 'Unknown relation name'
622
- end
623
- end
624
-
625
- fields = tables.collect { |t| "#{t}.*" }.join(',')
626
-
627
- update_condition options, join_conditions.join(' AND ')
628
- else
629
- fields = '*'
630
- end
631
-
632
- if join_table = options[:join_table]
633
- tables << join_table
634
- update_condition options, options[:join_condition]
635
- end
636
-
637
- if ogtype = options[:type]
638
- update_condition options, "ogtype='#{ogtype}'"
639
- end
640
-
641
- sql = "SELECT #{fields} FROM #{tables.join(',')}"
642
-
643
- if condition = options[:condition] || options[:where]
644
- sql << " WHERE #{condition}"
645
- end
646
-
647
- if order = options[:order]
648
- sql << " ORDER BY #{order}"
649
- end
650
-
651
- if offset = options[:offset]
652
- sql << " OFFSET #{offset}"
653
- end
654
-
655
- if limit = options[:limit]
656
- sql << " LIMIT #{limit}"
657
- end
658
-
659
- if extra = options[:extra]
660
- sql << " #{extra}"
661
- end
662
-
663
- return sql
664
- end
665
-
666
- # :section: Deserialization methods.
667
-
668
- # Deserialize the join relations.
669
-
670
- def read_join_relations(obj, res_row, row, join_relations)
671
- offset = obj.class.properties.size
672
-
673
- for rel in join_relations
674
- rel_obj = rel[:target_class].og_allocate(res_row)
675
- rel_obj.og_read(res_row, row, offset)
676
- offset += rel_obj.class.properties.size
677
- obj.instance_variable_set("@#{rel[:name]}", rel_obj)
678
- end
679
- end
680
-
681
- # Deserialize one object from the ResultSet.
682
-
683
- def read_one(res, klass, join_relations = nil)
684
- return nil if res.blank?
685
-
686
- if join_relations
687
- join_relations = [join_relations].flatten.collect do |n|
688
- klass.relation(n)
689
- end
690
- end
691
-
692
- res_row = res.next
693
-
694
- obj = klass.og_allocate(res_row)
695
- obj.og_read(res_row)
696
- read_join_relations(obj, res_row, 0, join_relations) if join_relations
697
-
698
- return obj
699
-
700
- ensure
701
- res.close
702
- end
703
-
704
- # Deserialize all objects from the ResultSet.
705
-
706
- def read_all(res, klass, join_relations = nil)
707
- return [] if res.blank?
708
-
709
- if join_relations
710
- join_relations = [join_relations].flatten.collect do |n|
711
- klass.relation(n)
712
- end
713
- end
714
-
715
- objects = []
716
-
717
- res.each_row do |res_row, row|
718
- obj = klass.og_allocate(res_row)
719
- obj.og_read(res_row, row)
720
- read_join_relations(obj, res_row, row, join_relations) if join_relations
721
- objects << obj
722
- end
723
-
724
- return objects
725
-
726
- ensure
727
- res.close
728
- end
729
-
730
- # Helper method that updates the condition string.
731
-
732
- def update_condition(options, cond, joiner = 'AND')
733
- if options[:condition]
734
- options[:condition] += " #{joiner} #{cond}"
735
- else
736
- options[:condition] = cond
737
- end
738
- end
589
+ return "#{self.class}.parse_blob(res[#{col} + offset])"
590
+ else
591
+ return "YAML::load(res[#{col} + offset])"
592
+ end
593
+ end
594
+
595
+ # :section: Lifecycle method compilers.
596
+
597
+ # Compile the og_insert method for the class.
598
+
599
+ def eval_og_insert(klass)
600
+ pk = klass.pk_symbol
601
+ props = klass.properties
602
+ values = props.collect { |p| write_prop(p) }.join(',')
603
+
604
+ if klass.metadata.superclass or klass.metadata.subclasses
605
+ props << Property.new(:ogtype, String)
606
+ values << ", '#{klass}'"
607
+ end
608
+
609
+ sql = "INSERT INTO #{klass.table} (#{props.collect {|p| p.symbol.to_s}.join(',')}) VALUES (#{values})"
610
+
611
+ klass.module_eval %{
612
+ def og_insert(store)
613
+ #{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
614
+ store.exec "#{sql}"
615
+ #{Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
616
+ end
617
+ }
618
+ end
619
+
620
+ # Compile the og_update method for the class.
621
+
622
+ def eval_og_update(klass)
623
+ pk = klass.pk_symbol
624
+ props = klass.properties.reject { |p| pk == p.symbol }
625
+
626
+ updates = props.collect { |p|
627
+ "#{p.symbol}=#{write_prop(p)}"
628
+ }
629
+
630
+ sql = "UPDATE #{klass::OGTABLE} SET #{updates.join(', ')} WHERE #{pk}=#\{@#{pk}\}"
631
+
632
+ klass.module_eval %{
633
+ def og_update(store, options = nil)
634
+ #{Aspects.gen_advice_code(:og_update, klass.advices, :pre) if klass.respond_to?(:advices)}
635
+ sql = "#{sql}"
636
+ sql << " AND \#{options[:condition]}" if options and options[:condition]
637
+ changed = store.sql_update(sql)
638
+ #{Aspects.gen_advice_code(:og_update, klass.advices, :post) if klass.respond_to?(:advices)}
639
+ return changed
640
+ end
641
+ }
642
+ end
643
+
644
+ # Compile the og_read method for the class. This method is
645
+ # used to read (deserialize) the given class from the store.
646
+ # In order to allow for changing field/attribute orders a
647
+ # field mapping hash is used.
648
+
649
+ def eval_og_read(klass)
650
+ code = []
651
+ props = klass.properties
652
+ field_map = create_field_map(klass)
653
+
654
+ props.each do |p|
655
+ if col = field_map[p.symbol]
656
+ code << "@#{p.symbol} = #{read_prop(p, col)}"
657
+ end
658
+ end
659
+
660
+ code = code.join('; ')
661
+
662
+ klass.module_eval %{
663
+ def og_read(res, row = 0, offset = 0)
664
+ #{Aspects.gen_advice_code(:og_read, klass.advices, :pre) if klass.respond_to?(:advices)}
665
+ #{code}
666
+ #{Aspects.gen_advice_code(:og_read, klass.advices, :post) if klass.respond_to?(:advices)}
667
+ end
668
+ }
669
+ end
670
+
671
+ #--
672
+ # FIXME: is pk needed as parameter?
673
+ #++
674
+
675
+ def eval_og_delete(klass)
676
+ klass.module_eval %{
677
+ def og_delete(store, pk, cascade = true)
678
+ #{Aspects.gen_advice_code(:og_delete, klass.advices, :pre) if klass.respond_to?(:advices)}
679
+ pk ||= @#{klass.pk_symbol}
680
+ transaction do |tx|
681
+ tx.exec "DELETE FROM #{klass.table} WHERE #{klass.pk_symbol}=\#{pk}"
682
+ if cascade and #{klass}.metadata[:descendants]
683
+ #{klass}.metadata[:descendants].each do |dclass, foreign_key|
684
+ tx.exec "DELETE FROM \#{dclass::OGTABLE} WHERE \#{foreign_key}=\#{pk}"
685
+ end
686
+ end
687
+ end
688
+ #{Aspects.gen_advice_code(:og_delete, klass.advices, :post) if klass.respond_to?(:advices)}
689
+ end
690
+ }
691
+ end
692
+
693
+ # Creates the schema for this class. Can be intercepted with
694
+ # aspects to add special behaviours.
695
+
696
+ def eval_og_create_schema(klass)
697
+ klass.module_eval %{
698
+ def og_create_schema(store)
699
+ #{Aspects.gen_advice_code(:og_create_schema, klass.advices, :pre) if klass.respond_to?(:advices)}
700
+ store.send(:create_table, #{klass}) if Og.create_schema
701
+ #{Aspects.gen_advice_code(:og_create_schema, klass.advices, :post) if klass.respond_to?(:advices)}
702
+ end
703
+ }
704
+ end
705
+
706
+ # :section: Misc methods.
707
+
708
+ def handle_sql_exception(ex, sql = nil)
709
+ Logger.error "DB error #{ex}, [#{sql}]"
710
+ Logger.error ex.backtrace.join("\n")
711
+ raise StoreException.new(ex, sql) if Og.raise_store_exceptions
712
+
713
+ # FIXME: should return :error or something.
714
+ return nil
715
+ end
716
+
717
+ def resolve_options(klass, options)
718
+ if sql = options[:sql]
719
+ sql = "SELECT * FROM #{klass.table} " + sql unless sql =~ /SELECT/
720
+ return sql
721
+ end
722
+
723
+ tables = [klass::OGTABLE]
724
+
725
+ if included = options[:include]
726
+ join_conditions = []
727
+
728
+ for name in [included].flatten
729
+ if rel = klass.relation(name)
730
+ target_table = rel[:target_class]::OGTABLE
731
+ tables << target_table
732
+ join_conditions << "#{klass::OGTABLE}.#{rel[:foreign_key]}=#{target_table}.#{rel[:target_pk]}"
733
+ else
734
+ raise 'Unknown relation name'
735
+ end
736
+ end
737
+
738
+ fields = tables.collect { |t| "#{t}.*" }.join(',')
739
+
740
+ update_condition options, join_conditions.join(' AND ')
741
+ elsif fields = options[:select]
742
+ # query the provided fields.
743
+ else
744
+ fields = '*'
745
+ end
746
+
747
+ if join_table = options[:join_table]
748
+ tables << join_table
749
+ update_condition options, options[:join_condition]
750
+ end
751
+
752
+ if ogtype = options[:type]
753
+ update_condition options, "ogtype='#{ogtype}'"
754
+ end
755
+
756
+ sql = "SELECT #{fields} FROM #{tables.join(',')}"
757
+
758
+ if condition = options[:condition] || options[:where]
759
+ sql << " WHERE #{condition}"
760
+ end
761
+
762
+ if order = options[:order]
763
+ sql << " ORDER BY #{order}"
764
+ end
765
+
766
+ resolve_limit_options(options, sql)
767
+
768
+ if extra = options[:extra]
769
+ sql << " #{extra}"
770
+ end
771
+
772
+ return sql
773
+ end
774
+
775
+ # Subclasses can override this if they need some other order.
776
+ # This is needed because different backends require different
777
+ # order of the keywords.
778
+
779
+ def resolve_limit_options(options, sql)
780
+ if limit = options[:limit]
781
+ sql << " LIMIT #{limit}"
782
+
783
+ if offset = options[:offset]
784
+ sql << " OFFSET #{offset}"
785
+ end
786
+ end
787
+ end
788
+
789
+ # :section: Deserialization methods.
790
+
791
+ # Read a field (column) from a result set row.
792
+
793
+ def read_field
794
+ end
795
+
796
+ # Dynamicaly deserializes a result set row into an object.
797
+ # Used for specialized queries or join queries. Please
798
+ # not that this deserialization method is slower than the
799
+ # precompiled og_read method.
800
+
801
+ def read_row(obj, res, res_row, row)
802
+ res.fields.each_with_index do |field, idx|
803
+ obj.instance_variable_set "@#{field}", res_row[idx]
804
+ end
805
+ end
806
+
807
+ # Deserialize the join relations.
808
+
809
+ def read_join_relations(obj, res_row, row, join_relations)
810
+ offset = obj.class.properties.size
811
+
812
+ for rel in join_relations
813
+ rel_obj = rel[:target_class].og_allocate(res_row)
814
+ rel_obj.og_read(res_row, row, offset)
815
+ offset += rel_obj.class.properties.size
816
+ obj.instance_variable_set("@#{rel[:name]}", rel_obj)
817
+ end
818
+ end
819
+
820
+ # Deserialize one object from the ResultSet.
821
+
822
+ def read_one(res, klass, options = nil)
823
+ return nil if res.blank?
824
+
825
+ if options and join_relations = options[:include]
826
+ join_relations = [join_relations].flatten.collect do |n|
827
+ klass.relation(n)
828
+ end
829
+ end
830
+
831
+ res_row = res.next
832
+
833
+ obj = klass.og_allocate(res_row)
834
+
835
+ if options and options[:select]
836
+ read_row(obj, res, res_row, 0)
837
+ else
838
+ obj.og_read(res_row)
839
+ read_join_relations(obj, res_row, 0, join_relations) if join_relations
840
+ end
841
+
842
+ return obj
843
+
844
+ ensure
845
+ res.close
846
+ end
847
+
848
+ # Deserialize all objects from the ResultSet.
849
+
850
+ def read_all(res, klass, options = nil)
851
+ return [] if res.blank?
852
+
853
+ if options and join_relations = options[:include]
854
+ join_relations = [join_relations].flatten.collect do |n|
855
+ klass.relation(n)
856
+ end
857
+ end
858
+
859
+ objects = []
860
+
861
+ if options and options[:select]
862
+ res.each_row do |res_row, row|
863
+ obj = klass.og_allocate(res_row)
864
+ read_row(obj, res, res_row, row)
865
+ objects << obj
866
+ end
867
+ else
868
+ res.each_row do |res_row, row|
869
+ obj = klass.og_allocate(res_row)
870
+ obj.og_read(res_row, row)
871
+ read_join_relations(obj, res_row, row, join_relations) if join_relations
872
+ objects << obj
873
+ end
874
+ end
875
+
876
+ return objects
877
+
878
+ ensure
879
+ res.close
880
+ end
881
+
882
+ # Helper method that updates the condition string.
883
+
884
+ def update_condition(options, cond, joiner = 'AND')
885
+ if options[:condition]
886
+ options[:condition] += " #{joiner} #{cond}"
887
+ else
888
+ options[:condition] = cond
889
+ end
890
+ end
739
891
 
740
892
  end
741
893
 
@@ -744,3 +896,4 @@ end
744
896
  # * George Moschovitis <gm@navel.gr>
745
897
  # * Michael Neumann <mneumann@ntecs.de>
746
898
  # * Ghislain Mary
899
+ # * Ysabel <deb@ysabel.org>