og 0.20.0 → 0.21.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.
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>