og 0.31.0 → 0.40.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 (97) hide show
  1. data/doc/{AUTHORS → CONTRIBUTORS} +26 -10
  2. data/doc/LICENSE +2 -3
  3. data/doc/RELEASES +56 -7
  4. data/doc/tutorial.txt +15 -15
  5. data/lib/glue/cacheable.rb +2 -5
  6. data/lib/glue/hierarchical.rb +1 -4
  7. data/lib/glue/optimistic_locking.rb +0 -2
  8. data/lib/glue/orderable.rb +79 -75
  9. data/lib/glue/revisable.rb +19 -24
  10. data/lib/glue/searchable.rb +0 -2
  11. data/lib/glue/taggable.rb +31 -29
  12. data/lib/glue/timestamped.rb +4 -2
  13. data/lib/og.rb +50 -29
  14. data/lib/og/adapter.rb +19 -0
  15. data/lib/og/adapter/mysql.rb +212 -0
  16. data/lib/og/adapter/mysql/override.rb +34 -0
  17. data/lib/og/adapter/mysql/script.rb +15 -0
  18. data/lib/og/adapter/mysql/utils.rb +40 -0
  19. data/lib/og/adapter/postgresql.rb +231 -0
  20. data/lib/og/adapter/postgresql/override.rb +117 -0
  21. data/lib/og/adapter/postgresql/script.rb +15 -0
  22. data/lib/og/adapter/postgresql/utils.rb +35 -0
  23. data/lib/og/adapter/sqlite.rb +132 -0
  24. data/lib/og/adapter/sqlite/override.rb +33 -0
  25. data/lib/og/adapter/sqlite/script.rb +15 -0
  26. data/lib/og/collection.rb +35 -7
  27. data/lib/og/{evolution.rb → dump.rb} +4 -5
  28. data/lib/og/entity.rb +102 -173
  29. data/lib/og/entity/clone.rb +119 -0
  30. data/lib/og/errors.rb +0 -2
  31. data/lib/og/manager.rb +85 -37
  32. data/lib/og/relation.rb +52 -34
  33. data/lib/og/relation/belongs_to.rb +0 -2
  34. data/lib/og/relation/has_many.rb +27 -4
  35. data/lib/og/relation/joins_many.rb +41 -14
  36. data/lib/og/relation/many_to_many.rb +10 -0
  37. data/lib/og/relation/refers_to.rb +22 -5
  38. data/lib/og/store.rb +80 -86
  39. data/lib/og/store/sql.rb +710 -713
  40. data/lib/og/store/sql/evolution.rb +119 -0
  41. data/lib/og/store/sql/join.rb +155 -0
  42. data/lib/og/store/sql/utils.rb +149 -0
  43. data/lib/og/test/assertions.rb +1 -3
  44. data/lib/og/test/testcase.rb +0 -2
  45. data/lib/og/types.rb +2 -5
  46. data/lib/og/validation.rb +6 -9
  47. data/test/{og/mixin → glue}/tc_hierarchical.rb +3 -13
  48. data/test/glue/tc_og_paginate.rb +47 -0
  49. data/test/{og/mixin → glue}/tc_optimistic_locking.rb +2 -12
  50. data/test/{og/mixin → glue}/tc_orderable.rb +15 -23
  51. data/test/glue/tc_orderable2.rb +47 -0
  52. data/test/glue/tc_revisable.rb +3 -3
  53. data/test/{og/mixin → glue}/tc_taggable.rb +20 -10
  54. data/test/{og/mixin → glue}/tc_timestamped.rb +2 -12
  55. data/test/glue/tc_webfile.rb +36 -0
  56. data/test/og/CONFIG.rb +8 -11
  57. data/test/og/multi_validations_model.rb +14 -0
  58. data/test/og/store/tc_filesys.rb +3 -1
  59. data/test/og/store/tc_kirby.rb +16 -13
  60. data/test/og/store/tc_sti.rb +11 -11
  61. data/test/og/store/tc_sti2.rb +79 -0
  62. data/test/og/tc_build.rb +41 -0
  63. data/test/og/tc_cacheable.rb +3 -2
  64. data/test/og/tc_has_many.rb +96 -0
  65. data/test/og/tc_inheritance.rb +6 -4
  66. data/test/og/tc_joins_many.rb +93 -0
  67. data/test/og/tc_multi_validations.rb +5 -7
  68. data/test/og/tc_multiple.rb +7 -6
  69. data/test/og/tc_override.rb +13 -7
  70. data/test/og/tc_primary_key.rb +30 -0
  71. data/test/og/tc_relation.rb +8 -14
  72. data/test/og/tc_reldelete.rb +163 -0
  73. data/test/og/tc_reverse.rb +17 -14
  74. data/test/og/tc_scoped.rb +3 -11
  75. data/test/og/tc_setup.rb +13 -11
  76. data/test/og/tc_store.rb +21 -28
  77. data/test/og/tc_validation2.rb +2 -2
  78. data/test/og/tc_validation_loop.rb +17 -15
  79. metadata +109 -103
  80. data/INSTALL +0 -91
  81. data/ProjectInfo +0 -51
  82. data/README +0 -177
  83. data/doc/config.txt +0 -28
  84. data/examples/README +0 -23
  85. data/examples/mysql_to_psql.rb +0 -71
  86. data/examples/run.rb +0 -271
  87. data/lib/glue/tree.rb +0 -218
  88. data/lib/og/store/alpha/filesys.rb +0 -110
  89. data/lib/og/store/alpha/memory.rb +0 -295
  90. data/lib/og/store/alpha/sqlserver.rb +0 -256
  91. data/lib/og/store/kirby.rb +0 -490
  92. data/lib/og/store/mysql.rb +0 -415
  93. data/lib/og/store/psql.rb +0 -875
  94. data/lib/og/store/sqlite.rb +0 -348
  95. data/lib/og/store/sqlite2.rb +0 -241
  96. data/setup.rb +0 -1585
  97. data/test/og/tc_sti_find.rb +0 -35
@@ -1,280 +1,31 @@
1
- require 'yaml'
2
- require 'time'
3
-
4
- require 'facet/kernel/constant'
5
- require 'facet/string/capitalized'
6
- require 'facet/ormsupport'
1
+ require 'facets/core/module/ancestor'
2
+ require 'facets/core/class/subclasses'
7
3
 
4
+ require 'og/store'
5
+ require 'og/store/sql/utils'
6
+ require 'og/store/sql/join'
7
+ require 'og/store/sql/evolution'
8
8
 
9
9
  module Og
10
10
 
11
- # A collection of useful SQL utilities.
12
-
13
- module SqlUtils
14
-
15
- # Escape an SQL string
16
-
17
- def escape(str)
18
- return nil unless str
19
- return str.gsub(/'/, "''")
20
- end
21
-
22
- # Convert a ruby time to an sql timestamp.
23
- #--
24
- # TODO: Optimize this.
25
- #++
26
-
27
- def timestamp(time = Time.now)
28
- return nil unless time
29
- return time.strftime("%Y-%m-%d %H:%M:%S")
30
- end
31
-
32
- # Output YYY-mm-dd
33
- #--
34
- # TODO: Optimize this.
35
- #++
36
-
37
- def date(date)
38
- return nil unless date
39
- return "#{date.year}-#{date.month}-#{date.mday}"
40
- end
41
-
42
- #--
43
- # TODO: implement me!
44
- #++
45
-
46
- def blob(val)
47
- val
48
- end
49
-
50
- # Parse an integer.
51
-
52
- def parse_int(int)
53
- int = int.to_i if int
54
- int
55
- end
56
-
57
- # Parse a float.
58
-
59
- def parse_float(fl)
60
- fl = fl.to_f if fl
61
- fl
62
- end
63
-
64
- # Parse sql datetime
65
- #--
66
- # TODO: Optimize this.
67
- #++
68
-
69
- def parse_timestamp(str)
70
- return nil unless str
71
- return Time.parse(str)
72
- end
73
-
74
- # Input YYYY-mm-dd
75
- #--
76
- # TODO: Optimize this.
77
- #++
78
-
79
- def parse_date(str)
80
- return nil unless str
81
- return Date.strptime(str)
82
- end
83
-
84
- # Parse a boolean
85
- # true, 1, t => true
86
- # other => false
87
-
88
- def parse_boolean(str)
89
- return true if (str=='true' || str=='t' || str=='1')
90
- return false
91
- end
92
-
93
- #--
94
- # TODO: implement me!!
95
- #++
96
-
97
- def parse_blob(val)
98
- val
99
- end
100
-
101
- # Escape the various Ruby types.
102
-
103
- def quote(vals)
104
- vals = [vals] unless vals.is_a?(Array)
105
- quoted = vals.inject("") do |s,val|
106
- s += case val
107
- when Fixnum, Integer, Float
108
- val ? val.to_s : 'NULL'
109
- when String
110
- val ? "'#{escape(val)}'" : 'NULL'
111
- when Time
112
- val ? "'#{timestamp(val)}'" : 'NULL'
113
- when Date
114
- val ? "'#{date(val)}'" : 'NULL'
115
- when TrueClass
116
- val ? "'t'" : 'NULL'
117
- else
118
- # gmosx: keep the '' for nil symbols.
119
- val ? escape(val.to_yaml) : ''
120
- end + ','
121
- end
122
- quoted.chop!
123
- vals.size > 1 ? "(#{quoted})" : quoted
124
- end
125
-
126
- # Escape the Array Ruby type.
127
-
128
- def quote_array(val)
129
- case val
130
- when Array
131
- val.collect{ |v| quotea(v) }.join(',')
132
- else
133
- quote(val)
134
- end
135
- end
136
- alias_method :quotea, :quote_array
137
-
138
- # Apply table name conventions to a class name.
139
-
140
- def tableize(klass)
141
- "#{klass.to_s.gsub(/::/, "_").downcase}"
142
- end
143
-
144
- def table(klass)
145
- klass.ann.self[:sql_table] || klass.ann.self[:table] || "#{Og.table_prefix}#{tableize(klass)}"
146
- end
147
-
148
- def join_object_ordering(obj1, obj2)
149
- if obj1.class.to_s <= obj2.class.to_s
150
- return obj1, obj2
151
- else
152
- return obj2, obj1, true
153
- end
154
- end
155
-
156
- def join_class_ordering(class1, class2)
157
- if class1.to_s <= class2.to_s
158
- return class1, class2
159
- else
160
- return class2, class1, true
161
- end
162
- end
163
-
164
- def build_join_name(class1, class2, postfix = nil)
165
- # Don't reorder arguments, as this is used in places that
166
- # have already determined the order they want.
167
- "#{Og.table_prefix}j_#{tableize(class1)}_#{tableize(class2)}#{postfix}"
168
- end
169
-
170
- def join_table(class1, class2, postfix = nil)
171
- first, second = join_class_ordering(class1, class2)
172
- build_join_name(first, second, postfix)
173
- end
174
-
175
- def join_table_index(key)
176
- "#{key}_idx"
177
- end
178
-
179
- def join_table_key(klass)
180
- klass = klass.schema_inheritance_root_class if klass.schema_inheritance_child?
181
- "#{klass.to_s.demodulize.underscore.downcase}_oid"
182
- end
183
-
184
- def join_table_keys(class1, class2)
185
- if class1 == class2
186
- # Fix for the self-join case.
187
- return join_table_key(class1), "#{join_table_key(class2)}2"
188
- else
189
- return join_table_key(class1), join_table_key(class2)
190
- end
191
- end
192
-
193
- def ordered_join_table_keys(class1, class2)
194
- first, second = join_class_ordering(class1, class2)
195
- return join_table_keys(first, second)
196
- end
197
-
198
- def join_table_info(relation, postfix = nil)
199
-
200
- # some fixes for schema inheritance.
201
-
202
- owner_class, target_class = relation.owner_class, relation.target_class
203
-
204
- raise "Undefined owner_class in #{target_class}" unless owner_class
205
- raise "Undefined target_class in #{owner_class}" unless target_class
206
-
207
- owner_class = owner_class.schema_inheritance_root_class if owner_class.schema_inheritance_child?
208
- target_class = target_class.schema_inheritance_root_class if target_class.schema_inheritance_child?
209
-
210
- owner_key, target_key = join_table_keys(owner_class, target_class)
211
- first, second, changed = join_class_ordering(owner_class, target_class)
212
-
213
- if changed
214
- first_key, second_key = target_key, owner_key
215
- else
216
- first_key, second_key = owner_key, target_key
217
- end
218
-
219
- table = (relation.table ?
220
- relation.table :
221
- join_table(owner_class, target_class, postfix)
222
- )
223
-
224
- return {
225
- :table => table,
226
- :owner_key => owner_key,
227
- :owner_table => table(owner_class),
228
- :target_key => target_key,
229
- :target_table => table(target_class),
230
- :first_table => table(first),
231
- :first_key => first_key,
232
- :first_index => join_table_index(first_key),
233
- :second_table => table(second),
234
- :second_key => second_key,
235
- :second_index => join_table_index(second_key)
236
- }
237
- end
238
-
239
- # Subclasses can override this if they need a different
240
- # syntax.
241
-
242
- def create_join_table_sql(join_table_info, suffix = 'NOT NULL', key_type = 'integer')
243
- join_table = join_table_info[:table]
244
- first_index = join_table_info[:first_index]
245
- first_key = join_table_info[:first_key]
246
- second_key = join_table_info[:second_key]
247
- second_index = join_table_info[:second_index]
248
-
249
- sql = []
250
-
251
- sql << %{
252
- CREATE TABLE #{join_table} (
253
- #{first_key} integer NOT NULL,
254
- #{second_key} integer NOT NULL,
255
- PRIMARY KEY(#{first_key}, #{second_key})
256
- )
257
- }
258
-
259
- # gmosx: not that useful?
260
- # sql << "CREATE INDEX #{first_index} ON #{join_table} (#{first_key})"
261
- # sql << "CREATE INDEX #{second_index} ON #{join_table} (#{second_key})"
262
-
263
- return sql
264
- end
265
-
266
- end
267
-
268
- # An abstract SQL powered store.
11
+ # The base implementation for all SQL stores. Reused by all
12
+ # SQL adapters.
269
13
 
270
14
  class SqlStore < Store
271
- extend SqlUtils
272
- include SqlUtils
273
-
274
- # The connection to the backend SQL RDBMS.
275
15
 
16
+ # The connection to the SQL backend.
17
+
276
18
  attr_accessor :conn
19
+
20
+ # Ruby type <-> SQL type mappings.
21
+
22
+ attr_accessor :typemap
277
23
 
24
+ # Initialize the store.
25
+ #--
26
+ # Override in the adapter.
27
+ #++
28
+
278
29
  def initialize(options)
279
30
  super
280
31
 
@@ -297,42 +48,61 @@ class SqlStore < Store
297
48
  end
298
49
 
299
50
  #--
300
- # FIXME: not working.
51
+ # Override in the adapter.
301
52
  #++
302
53
 
303
- def enable_logging
304
- require 'facets/more/aspects'
305
- klass = self.class
306
- klass.send :include, ::Aspects
307
- klass.pre "Logger.info sql", :on => [:exec, :query]
308
- ::Aspects.wrap(klass, [:exec, :query])
54
+ def close
55
+ @conn.close
56
+ super
309
57
  end
310
58
 
311
- # Returns a list of tables that exist within the database but are
312
- # not managed by the supplied manager.
59
+ # Creates the database where Og managed objects are
60
+ # serialized.
61
+ #--
62
+ # Override in the adapter.
63
+ #++
313
64
 
314
- def unmanaged_tables(manager)
315
- ret = Array.new
316
- mt = managed_tables(manager)
317
- @conn.list_tables.each do |table|
318
- ret << table unless mt.include?(table)
319
- end
320
- ret
65
+ def create_db(options)
66
+ Logger.info "Created database '#{options[:name]}'"
67
+ end
68
+
69
+ # Destroys the database where Og managed objects are
70
+ # serialized.
71
+ #--
72
+ # Override in the adapter.
73
+ #++
74
+
75
+ def destroy_db(options)
76
+ Logger.info "Dropped database '#{options[:name]}'"
77
+ end
78
+ alias drop_db destroy_db
79
+
80
+ # The type used for default primary keys.
81
+
82
+ def primary_key_type
83
+ 'integer PRIMARY KEY'
321
84
  end
322
85
 
323
- # Returns a list of tables within the database that are there to
324
- # support a class managed by the supplied manager.
86
+ # Force the creation of a primary key class.
325
87
 
326
- def managed_tables(manager)
327
- ret = Array.new
328
- manager.managed_classes.each do |klass|
329
- ret << klass::OGTABLE
330
- ret.concat(klass.relations.reject{|rel| not rel.options[:join_table]}.map{|rel| rel.options[:join_table]})
88
+ def force_primary_key(klass)
89
+ # Automatically add an :oid serializable field if none is
90
+ # defined and no other primary key is defined.
91
+
92
+ if klass.primary_key == :oid and !klass.attributes.include?(:oid)
93
+ klass.attr_accessor :oid, Fixnum, :sql => primary_key_type
331
94
  end
332
- ret
333
95
  end
334
96
 
335
- # Enchants a class.
97
+ # Performs SQL related enchanting to the class. This method
98
+ # is further extended in more specialized adapters to add
99
+ # backend specific enchanting.
100
+ #
101
+ # Defines:
102
+ #
103
+ # * the OGTABLE constant, and .table / .schema aliases.
104
+ # * the index method for defing sql indices.
105
+ # * precompiles the object lifecycle callbacks.
336
106
 
337
107
  def enchant(klass, manager)
338
108
  # setup the table where this class is mapped.
@@ -344,15 +114,17 @@ class SqlStore < Store
344
114
  klass.const_set 'OGTABLE', table(klass)
345
115
  end
346
116
 
347
- #--
348
- # FIXME: use an SQL agnostic name like schema instead
349
- # of table.
350
- #++
117
+ # Define table and schema aliases for OGTABLE.
351
118
 
352
- klass.module_eval 'def self.table; OGTABLE; end'
119
+ klass.module_eval %{
120
+ def self.table; OGTABLE; end
121
+ def self.schema; OGTABLE; end
122
+ }
353
123
 
354
124
  eval_og_allocate(klass)
355
125
 
126
+ # Perform base store enchantment.
127
+
356
128
  super
357
129
 
358
130
  unless klass.polymorphic_parent?
@@ -381,10 +153,12 @@ class SqlStore < Store
381
153
  # :section: Lifecycle methods.
382
154
 
383
155
  # Loads an object from the store using the primary key.
384
-
156
+ # Returns nil if the passes pk is nil.
157
+
385
158
  def load(pk, klass)
386
- pk_field = klass.primary_key.field || klass.primary_key.symbol
387
- sql = "SELECT * FROM #{klass::OGTABLE} WHERE #{pk_field}=#{pk.to_i}"
159
+ return nil unless pk
160
+
161
+ sql = "SELECT * FROM #{klass.table} WHERE #{pk_field klass}=#{quote(pk)}"
388
162
  sql << " AND ogtype='#{klass}'" if klass.schema_inheritance_child?
389
163
  res = query sql
390
164
  read_one(res, klass)
@@ -392,36 +166,40 @@ class SqlStore < Store
392
166
  alias_method :exist?, :load
393
167
 
394
168
  # Reloads an object from the store.
169
+ # Returns nil if the passes pk is nil.
395
170
 
396
171
  def reload(obj, pk)
172
+ return nil unless pk
173
+
174
+ klass = obj.class
397
175
  raise 'Cannot reload unmanaged object' unless obj.saved?
398
- sql = "SELECT * FROM #{obj.class.table} WHERE #{obj.class.pk_symbol}=#{pk.to_i}"
399
- sql << " AND ogtype='#{obj.class}'" if obj.class.schema_inheritance_child?
176
+ sql = "SELECT * FROM #{klass.table} WHERE #{pk_field klass}=#{quote(pk)}"
177
+ sql << " AND ogtype='#{klass}'" if klass.schema_inheritance_child?
400
178
  res = query sql
401
179
  obj.og_read(res.next, 0)
402
180
  ensure
403
181
  res.close if res
404
182
  end
405
183
 
406
- # If a properties collection is provided, only updates the
407
- # selected properties. Pass the required properties as symbols
184
+ # If an attributes collection is provided, only updates the
185
+ # selected attributes. Pass the required attributes as symbols
408
186
  # or strings.
409
187
  #--
410
188
  # gmosx, THINK: condition is not really useful here :(
411
189
  #++
412
190
 
413
191
  def update(obj, options = nil)
414
- if options and properties = options[:only]
415
- if properties.is_a?(Array)
192
+ if options and attrs = options[:only]
193
+ if attrs.is_a?(Array)
416
194
  set = []
417
- for p in properties
418
- set << "#{p}=#{quote(obj.send(p))}"
195
+ for a in attrs
196
+ set << "#{a}=#{quote(obj.send(a))}"
419
197
  end
420
198
  set = set.join(',')
421
199
  else
422
- set = "#{properties}=#{quote(obj.send(properties))}"
200
+ set = "#{attrs}=#{quote(obj.send(attrs))}"
423
201
  end
424
- sql = "UPDATE #{obj.class.table} SET #{set} WHERE #{obj.class.pk_symbol}=#{obj.pk}"
202
+ sql = "UPDATE #{obj.class.table} SET #{set} WHERE #{pk_field obj.class}=#{quote(obj.pk)}"
425
203
  sql << " AND #{options[:condition]}" if options[:condition]
426
204
  sql_update(sql)
427
205
  else
@@ -429,26 +207,17 @@ class SqlStore < Store
429
207
  end
430
208
  end
431
209
 
432
- # Update selected properties of an object or class of
433
- # objects.
434
-
435
- def update_properties(target, *properties)
436
- update(target, :only => properties)
437
- end
438
- alias_method :pupdate, :update_properties
439
- alias_method :update_property, :update_properties
440
-
441
210
  # More generalized method, also allows for batch updates.
442
211
 
443
212
  def update_by_sql(target, set, options = nil)
444
213
  set = set.gsub(/@/, '')
445
214
 
446
- if target.is_a?(Class)
215
+ if target.is_a? Class
447
216
  sql = "UPDATE #{target.table} SET #{set} "
448
217
  sql << " WHERE #{options[:condition]}" if options and options[:condition]
449
218
  sql_update(sql)
450
219
  else
451
- sql = "UPDATE #{target.class.table} SET #{set} WHERE #{target.class.pk_symbol}=#{target.pk}"
220
+ sql = "UPDATE #{target.class.table} SET #{set} WHERE #{pk_field target.class}=#{quote(target.pk)}"
452
221
  sql << " AND #{options[:condition]}" if options and options[:condition]
453
222
  sql_update(sql)
454
223
  end
@@ -498,6 +267,10 @@ class SqlStore < Store
498
267
  # This low level method is used by the Entity
499
268
  # calculation / aggregation methods.
500
269
  #
270
+ # === Options
271
+ #
272
+ # :field = the return type.
273
+ #
501
274
  # === Example
502
275
  #
503
276
  # calculate 'COUNT(*)'
@@ -505,152 +278,128 @@ class SqlStore < Store
505
278
  # calculate 'SUM(age)', :group => :name
506
279
 
507
280
  def aggregate(term = 'COUNT(*)', options = {})
281
+ # Leave this .dup here, causes problems because options are changed
282
+ options = options.dup
283
+
508
284
  klass = options[:class]
509
285
  field = options[:field]
510
-
511
- if field_properties = klass.properties[field]
512
- return_type = field_properties[:klass]
286
+
287
+ # Rename search term, SQL92 but _not_ SQL89 compatible
288
+ options.update(:select => "#{term} AS #{term[/^\w+/]}")
289
+
290
+ unless options[:group] || options[:group_by]
291
+ options.delete(:order)
292
+ options.delete(:order_by)
513
293
  end
514
-
515
- if options.is_a?(String)
516
- sql = options
517
- else
518
- sql = "SELECT #{term} FROM #{options[:class].table}"
519
- if condition = options[:condition]
520
- sql << " WHERE #{condition}"
521
- sql << " AND " if options[:class].schema_inheritance_child?
522
- else
523
- sql << " WHERE " if options[:class].schema_inheritance_child?
524
- end
525
- sql << "ogtype='#{options[:class]}'" if options[:class].schema_inheritance_child?
526
- if group = options[:group]
527
- sql << " GROUP BY #{group}"
528
- end
529
- if extra = options[:extra_sql]
530
- sql << " {extra}"
531
- end
294
+
295
+ sql = resolve_options(klass, options)
296
+
297
+ if anno = klass.ann[field]
298
+ return_type = anno.class
532
299
  end
533
-
534
- if group
300
+ return_type ||= Integer
301
+
302
+ if options[:group] || options[:group_by]
535
303
  # This is an aggregation, so return the calculated values
536
304
  # as an array.
537
305
  values = []
538
306
  res = query(sql)
539
307
  res.each_row do |row, idx|
540
- value = [ row[idx] ].flatten[0]
541
- values << type_cast(return_type, value)
308
+ values << type_cast(return_type, row[0])
542
309
  end
543
310
  return values
544
311
  else
545
- #--
546
- # gmosx, TODO: don't convert to float by default, perhaps
547
- # should consult an option.
548
- #++
549
312
  return type_cast(return_type, query(sql).first_value)
550
313
  end
314
+
551
315
  end
552
316
  alias_method :calculate, :aggregate
553
317
 
554
- def type_cast(klass, val)
555
- typemap = {
556
- Time => :parse_timestamp,
557
- Date => :parse_date,
558
- }
559
-
560
- if method = typemap[klass]
561
- send(method, val)
562
- else
563
- val.to_f
564
- end
565
- end
566
-
567
318
  # Perform a count query.
568
319
 
569
320
  def count(options = {})
570
321
  calculate('COUNT(*)', options).to_i
571
322
  end
572
323
 
573
- # Relate two objects through an intermediate join table.
574
- # Typically used in joins_many and many_to_many relations.
575
-
576
- def join(obj1, obj2, table, options = nil)
577
- first, second = join_object_ordering(obj1, obj2)
578
- first_key, second_key = ordered_join_table_keys(obj1.class, obj2.class)
579
- if options
580
- exec "INSERT INTO #{table} (#{first_key},#{second_key}, #{options.keys.join(',')}) VALUES (#{first.pk},#{second.pk}, #{options.values.map { |v| quote(v) }.join(',')})"
581
- else
582
- exec "INSERT INTO #{table} (#{first_key},#{second_key}) VALUES (#{first.pk}, #{second.pk})"
583
- end
584
- end
585
-
586
- # Unrelate two objects be removing their relation from the
587
- # join table.
588
-
589
- def unjoin(obj1, obj2, table)
590
- first, second = join_object_ordering(obj1, obj2)
591
- first_key, second_key = ordered_join_table_keys(obj1.class, obj2.class)
592
- exec "DELETE FROM #{table} WHERE #{first_key}=#{first.pk} AND #{second_key}=#{second.pk}"
593
- end
594
-
324
+ # Delete all instances of the given class from the backend.
325
+
595
326
  def delete_all(klass)
596
327
  sql = "DELETE FROM #{klass.table}"
597
328
  sql << " WHERE ogtype='#{klass}'" if klass.schema_inheritance? and not klass.schema_inheritance_root?
598
329
  exec sql
599
330
  end
331
+
332
+ # :section: Misc methods.
333
+
334
+ # Create the SQL table where instances of the given class
335
+ # will be serialized.
336
+
337
+ def create_table(klass)
338
+ fields = fields_for_class(klass)
600
339
 
601
- # :section: Transaction methods.
602
-
603
- # Start a new transaction.
604
-
605
- def start
606
- exec('START TRANSACTION') if @transaction_nesting < 1
607
- @transaction_nesting += 1
608
- end
609
-
610
- # Commit a transaction.
340
+ sql = "CREATE TABLE #{klass.table} (#{fields.join(', ')}"
611
341
 
612
- def commit
613
- @transaction_nesting -= 1
614
- exec('COMMIT') if @transaction_nesting < 1
615
- end
342
+ # Create table constraints.
616
343
 
617
- # Rollback a transaction.
344
+ if constraints = klass.ann.self[:sql_constraint]
345
+ sql << ", #{constraints.join(', ')}"
346
+ end
618
347
 
619
- def rollback
620
- @transaction_nesting -= 1
621
- exec('ROLLBACK') if @transaction_nesting < 1
622
- end
348
+ # Set the table type (Mysql default, InnoDB, Hash, etc)
349
+
350
+ if table_type = @options[:table_type]
351
+ sql << ") TYPE = #{table_type};"
352
+ else
353
+ sql << ");"
354
+ end
623
355
 
624
- # :section: Low level methods.
356
+ # Create indices.
357
+ #
358
+ # An example index definition:
359
+ #
360
+ # classs MyClass
361
+ # attr_accessor :age, Fixnum, :index => true, :pre_index => ..., :post_index => ...
362
+ # end
363
+
364
+ for idx in sql_indices_for_class(klass)
365
+ anno = klass.ann(idx)
366
+ idx = idx.to_s
367
+ pre_sql, post_sql = anno[:pre_index], options[:post_index]
368
+ idxname = idx.gsub(/ /, "").gsub(/,/, "_").gsub(/\(.*\)/, "")
369
+ sql << " CREATE #{pre_sql} INDEX #{klass.table}_#{idxname}_idx #{post_sql} ON #{klass.table} (#{idx});"
370
+ end
625
371
 
626
- # Encapsulates a low level update method.
372
+ begin
373
+ exec(sql, false)
374
+ Logger.info "Created table '#{klass.table}'."
375
+ rescue Object => ex
376
+ if table_already_exists_exception? ex
377
+ # Don't return yet. Fall trough to also check for the
378
+ # join table.
379
+ else
380
+ handle_sql_exception(ex, sql)
381
+ end
382
+ end
627
383
 
628
- def sql_update(sql)
629
- exec(sql)
630
- # return affected rows.
631
- end
384
+ # Create join tables if needed. Join tables are used in
385
+ # 'many_to_many' relations.
632
386
 
633
- #takes an array, the first parameter of which is a prepared statement
634
- #style string; this handles parameter escaping.
635
- def prepare_statement(condition)
636
- if condition.is_a?(Array)
637
- args = condition.dup
638
- str = args.shift
639
- # ? handles a single type.
640
- # ?* handles an array.
641
- args.each { |arg| str.sub!(/\?\*/, quotea(arg)); str.sub!(/\?/, quote(arg)) }
642
- condition = str
387
+ if join_tables = klass.ann.self[:join_tables]
388
+ for info in join_tables
389
+ begin
390
+ create_join_table_sql(info).each do |sql|
391
+ exec(sql, false)
392
+ end
393
+ Logger.debug "Created jointable '#{info[:table]}'." if $DBG
394
+ rescue Object => ex
395
+ if table_already_exists_exception? ex
396
+ Logger.debug 'Join table already exists' if $DBG
397
+ else
398
+ raise
399
+ end
400
+ end
401
+ end
643
402
  end
644
- condition
645
- end
646
-
647
- private
648
-
649
- # Create the sql table where objects of this class are
650
- # persisted.
651
-
652
- def create_table(klass)
653
- raise 'Not implemented'
654
403
  end
655
404
 
656
405
  # Drop the sql table where objects of this class are
@@ -669,290 +418,50 @@ private
669
418
  alias_method :destroy, :drop_table
670
419
  alias_method :drop_schema, :drop_schema
671
420
 
672
- # Evolve (recreate) the sql table where objects of this class
673
- # are persisted.
674
-
675
- def evolve_table(klass)
676
- drop_table(klass)
677
- create_table(klass)
678
- end
679
- alias_method :update_table, :evolve_table
421
+ # Perform an sql query with results.
680
422
 
681
- # Return the field for the given property.
682
-
683
- def field_for_property(property)
684
- if f = property.field
685
- return f.to_s
686
- else
687
- return property.to_s
423
+ def query(sql, rescue_exception = true)
424
+ Logger.debug sql if $DBG
425
+ return query_statement(sql)
426
+ rescue Object => ex
427
+ if rescue_exception
428
+ handle_sql_exception(ex, sql)
429
+ else
430
+ raise
688
431
  end
689
432
  end
433
+
434
+ #--
435
+ # Override.
436
+ #++
690
437
 
691
- #utility function - return either the properties for the class
692
- #or, in the case of schema inheritance, all of the properties
693
- #of the class hierarchy, starting from the schema inheritance
694
- #root class
695
-
696
- def get_properties_for_class(klass)
697
- properties = klass.properties.dup
698
- if(klass.schema_inheritance?)
699
- for desc in klass.schema_inheritance_root_class.descendents
700
- properties.update(desc.properties)
701
- end
702
- end
703
- properties
438
+ def query_statement(sql)
439
+ return @conn.query(sql)
704
440
  end
705
-
706
- # Create the fields that correspond to the klass properties.
707
- # The generated fields array is used in create_table.
708
- # If the property has an :sql annotation this overrides the
709
- # default mapping. If the property has an :extra_sql annotation
710
- # the extra sql is appended after the default mapping.
711
441
 
712
- def fields_for_class(klass)
713
- fields = []
714
- properties = get_properties_for_class(klass)
715
-
716
- if klass.schema_inheritance?
717
- # This class as a superclass in a single table inheritance
718
- # chain. So inject a special class ogtype field that
719
- # holds the class name.
720
-
721
- fields << "ogtype VARCHAR(50)"
722
- end
723
-
724
- for p in properties.values
725
- klass.index(p.symbol) if p.index
726
-
727
- field = field_for_property(p)
728
-
729
- if p.sql
730
- field << " #{p.sql}"
731
- else
732
- field << " #{type_for_class(p.klass)}"
733
- field << " UNIQUE" if p.unique
734
- field << " DEFAULT #{p.default.inspect} NOT NULL" if p.default
735
- field << " #{p.extra_sql}" if p.extra_sql
736
- end
737
-
738
- fields << field
739
- end
740
-
741
- return fields
742
- end
442
+ # Perform an sql query with no results.
743
443
 
744
- def type_for_class(klass)
745
- @typemap[klass]
444
+ def exec(sql, rescue_exception = true)
445
+ Logger.debug sql if $DBG
446
+ exec_statement(sql)
447
+ rescue Object => ex
448
+ if rescue_exception
449
+ handle_sql_exception(ex, sql)
450
+ else
451
+ raise
452
+ end
746
453
  end
747
454
 
748
- # Return an sql string evaluator for the property.
749
- # No need to optimize this, used only to precalculate code.
750
- # YAML is used to store general Ruby objects to be more
751
- # portable.
752
455
  #--
753
- # FIXME: add extra handling for float.
456
+ # Override.
754
457
  #++
755
458
 
756
- def write_prop(p)
757
- if p.klass.ancestors.include?(Integer)
758
- return "#\{@#{p} || 'NULL'\}"
759
- elsif p.klass.ancestors.include?(Float)
760
- return "#\{@#{p} || 'NULL'\}"
761
- elsif p.klass.ancestors.include?(String)
762
- return %|#\{@#{p} ? "'#\{#{self.class}.escape(@#{p})\}'" : 'NULL'\}|
763
- elsif p.klass.ancestors.include?(Time)
764
- return %|#\{@#{p} ? "'#\{#{self.class}.timestamp(@#{p})\}'" : 'NULL'\}|
765
- elsif p.klass.ancestors.include?(Date)
766
- return %|#\{@#{p} ? "'#\{#{self.class}.date(@#{p})\}'" : 'NULL'\}|
767
- elsif p.klass.ancestors.include?(TrueClass)
768
- return "#\{@#{p} ? \"'t'\" : 'NULL' \}"
769
- elsif p.klass.ancestors.include?(Og::Blob)
770
- return %|#\{@#{p} ? "'#\{#{self.class}.escape(#{self.class}.blob(@#{p}))\}'" : 'NULL'\}|
771
- else
772
- # gmosx: keep the '' for nil symbols.
773
- return %|#\{@#{p} ? "'#\{#{self.class}.escape(@#{p}.to_yaml)\}'" : "''"\}|
774
- end
775
- end
459
+ def exec_statement(sql)
460
+ return @conn.exec(sql)
461
+ end
776
462
 
777
- # Return an evaluator for reading the property.
778
- # No need to optimize this, used only to precalculate code.
779
-
780
- def read_prop(p, col)
781
- if p.klass.ancestors.include?(Integer)
782
- return "#{self.class}.parse_int(res[#{col} + offset])"
783
- elsif p.klass.ancestors.include?(Float)
784
- return "#{self.class}.parse_float(res[#{col} + offset])"
785
- elsif p.klass.ancestors.include?(String)
786
- return "res[#{col} + offset]"
787
- elsif p.klass.ancestors.include?(Time)
788
- return "#{self.class}.parse_timestamp(res[#{col} + offset])"
789
- elsif p.klass.ancestors.include?(Date)
790
- return "#{self.class}.parse_date(res[#{col} + offset])"
791
- elsif p.klass.ancestors.include?(TrueClass)
792
- return "#{self.class}.parse_boolean(res[#{col} + offset])"
793
- elsif p.klass.ancestors.include?(Og::Blob)
794
- return "#{self.class}.parse_blob(res[#{col} + offset])"
795
- else
796
- return "YAML::load(res[#{col} + offset])"
797
- end
798
- end
799
-
800
- # :section: Lifecycle method compilers.
801
-
802
-
803
- # Get the fields from the database table. Also handles the
804
- # change of ordering of the fields in the table.
805
- #
806
- # To ignore a database field use the ignore_fields annotation
807
- # ie,
808
- #
809
- # class Article
810
- # ann self, :ignore_fields => [ :tsearch_idx, :ext_field ]
811
- # end
812
- #
813
- # other aliases for ignore_fiels: ignore_field, ignore_column.
463
+ # Gracefully handle a backend exception.
814
464
 
815
- def create_field_map(klass)
816
- end
817
-
818
- # Compile the og_insert method for the class.
819
-
820
- def eval_og_insert(klass)
821
- pk = klass.pk_symbol
822
- props = klass.properties.values.dup
823
- values = props.collect { |p| write_prop(p) }.join(',')
824
-
825
- if klass.ann.self[:superclass] or klass.ann.self[:subclasses]
826
- props << Property.new(:symbol => :ogtype, :klass => String)
827
- values << ", '#{klass}'"
828
- end
829
-
830
- sql = "INSERT INTO #{klass.table} (#{props.collect {|p| field_for_property(p)}.join(',')}) VALUES (#{values})"
831
-
832
- klass.module_eval %{
833
- def og_insert(store)
834
- #{::Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
835
- store.exec "#{sql}"
836
- #{::Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
837
- end
838
- }
839
- end
840
-
841
- # Compile the og_update method for the class.
842
-
843
- def eval_og_update(klass)
844
- pk = klass.pk_symbol
845
- pk_field = klass.primary_key.field || klass.primary_key.symbol
846
-
847
- props = klass.properties.values.reject { |p| pk == p.symbol }
848
-
849
- updates = props.collect { |p|
850
- "#{field_for_property(p)}=#{write_prop(p)}"
851
- }
852
-
853
- sql = "UPDATE #{klass::OGTABLE} SET #{updates.join(', ')} WHERE #{pk_field}=#\{@#{pk}\}"
854
-
855
- klass.module_eval %{
856
- def og_update(store, options = nil)
857
- #{::Aspects.gen_advice_code(:og_update, klass.advices, :pre) if klass.respond_to?(:advices)}
858
- sql = "#{sql}"
859
- sql << " AND \#{options[:condition]}" if options and options[:condition]
860
- changed = store.sql_update(sql)
861
- #{::Aspects.gen_advice_code(:og_update, klass.advices, :post) if klass.respond_to?(:advices)}
862
- return changed
863
- end
864
- }
865
- end
866
-
867
- # Compile the og_read method for the class. This method is
868
- # used to read (deserialize) the given class from the store.
869
- # In order to allow for changing field/attribute orders a
870
- # field mapping hash is used.
871
-
872
- def eval_og_read(klass)
873
- code = []
874
- props = klass.properties.values
875
- field_map = create_field_map(klass)
876
-
877
- for p in props
878
- f = field_for_property(p).to_sym
879
-
880
- if col = field_map[f]
881
- code << "@#{p} = #{read_prop(p, col)}"
882
- end
883
- end
884
-
885
- code = code.join('; ')
886
-
887
- klass.module_eval %{
888
- def og_read(res, row = 0, offset = 0)
889
- #{::Aspects.gen_advice_code(:og_read, klass.advices, :pre) if klass.respond_to?(:advices)}
890
- #{code}
891
- #{::Aspects.gen_advice_code(:og_read, klass.advices, :post) if klass.respond_to?(:advices)}
892
- end
893
- }
894
- end
895
-
896
- #--
897
- # FIXME: is pk needed as parameter?
898
- #++
899
-
900
- def eval_og_delete(klass)
901
- klass.module_eval %{
902
- def og_delete(store, pk, cascade = true)
903
- #{::Aspects.gen_advice_code(:og_delete, klass.advices, :pre) if klass.respond_to?(:advices)}
904
- pk ||= @#{klass.pk_symbol}
905
- transaction do |tx|
906
- tx.exec "DELETE FROM #{klass.table} WHERE #{klass.pk_symbol}=\#{pk}"
907
- if cascade and #{klass}.ann.self[:descendants]
908
- #{klass}.ann.self.descendants.each do |dclass, foreign_key|
909
- tx.exec "DELETE FROM \#{dclass::OGTABLE} WHERE \#{foreign_key}=\#{pk}"
910
- end
911
- end
912
- end
913
- #{::Aspects.gen_advice_code(:og_delete, klass.advices, :post) if klass.respond_to?(:advices)}
914
- end
915
- }
916
- end
917
-
918
- # Creates the schema for this class. Can be intercepted with
919
- # aspects to add special behaviours.
920
-
921
- def eval_og_create_schema(klass)
922
- klass.module_eval %{
923
- def og_create_schema(store)
924
- if Og.create_schema
925
- #{::Aspects.gen_advice_code(:og_create_schema, klass.advices, :pre) if klass.respond_to?(:advices)}
926
- # unless self.class.superclass.ancestors.include? SchemaInheritanceBase
927
- store.send(:create_table, #{klass})
928
- # end
929
- #{::Aspects.gen_advice_code(:og_create_schema, klass.advices, :post) if klass.respond_to?(:advices)}
930
- end
931
- end
932
- }
933
- end
934
-
935
- # Precompile a class specific allocate method. If this is an
936
- # STI parent classes it reads the class from the resultset.
937
-
938
- def eval_og_allocate(klass)
939
- if klass.schema_inheritance?
940
- klass.module_eval %{
941
- def self.og_allocate(res, row = 0)
942
- Object.constant(res[0]).allocate
943
- end
944
- }
945
- else
946
- klass.module_eval %{
947
- def self.og_allocate(res, row = 0)
948
- self.allocate
949
- end
950
- }
951
- end
952
- end
953
-
954
- # :section: Misc methods.
955
-
956
465
  def handle_sql_exception(ex, sql = nil)
957
466
  Logger.error "DB error #{ex}, [#{sql}]"
958
467
  Logger.error ex.backtrace.join("\n")
@@ -962,6 +471,25 @@ private
962
471
  return nil
963
472
  end
964
473
 
474
+ # Perform an sql update, return the number of updated rows.
475
+ #--
476
+ # Override
477
+ #++
478
+
479
+ def sql_update(sql)
480
+ exec(sql)
481
+ # return affected rows.
482
+ end
483
+
484
+ # Return the last inserted row id.
485
+ #--
486
+ # Override
487
+ #--
488
+
489
+ def last_insert_id(klass = nil)
490
+ # return last insert id
491
+ end
492
+
965
493
  # Resolve the finder options. Also takes scope into account.
966
494
  # This method handles among other the following cases:
967
495
  #
@@ -1049,24 +577,42 @@ private
1049
577
  # If an array is passed as a condition, use prepared
1050
578
  # statement style escaping.
1051
579
 
1052
- condition = prepare_statement(condition)
580
+ if condition.is_a?(Array)
581
+ condition = prepare_statement(condition)
582
+ end
1053
583
 
1054
584
  sql << " WHERE #{condition}"
1055
585
  end
586
+
587
+ if group = options[:group] || options[:group_by]
588
+ sql << " GROUP BY #{group}"
589
+ end
1056
590
 
1057
- if order = options[:order]
591
+ if order = options[:order] || options[:order_by]
1058
592
  sql << " ORDER BY #{order}"
1059
593
  end
1060
594
 
1061
595
  resolve_limit_options(options, sql)
1062
596
 
1063
- if extra = options[:extra]
597
+ if extra = options[:extra] || options[:extra_sql]
1064
598
  sql << " #{extra}"
1065
599
  end
1066
600
 
1067
601
  return sql
1068
602
  end
1069
603
 
604
+ #takes an array, the first parameter of which is a prepared statement
605
+ #style string; this handles parameter escaping.
606
+
607
+ def prepare_statement(condition)
608
+ args = condition.dup
609
+ str = args.shift
610
+ # ? handles a single type.
611
+ # ?* handles an array.
612
+ args.each { |arg| str.sub!(/\?\*/, quotea(arg)); str.sub!(/\?/, quote(arg)) }
613
+ condition = str
614
+ end
615
+
1070
616
  # Subclasses can override this if they need some other order.
1071
617
  # This is needed because different backends require different
1072
618
  # order of the keywords.
@@ -1081,6 +627,414 @@ private
1081
627
  end
1082
628
  end
1083
629
 
630
+ # :section: Utility methods for serialization/deserialization.
631
+
632
+ # Return either the serializable attributes for the class or,
633
+ # in the case of schema inheritance, all of the serializable
634
+ # attributes of the class hierarchy, starting from the schema
635
+ # inheritance root class.
636
+
637
+ def serializable_attributes_for_class(klass)
638
+ attrs = klass.serializable_attributes
639
+ if klass.schema_inheritance?
640
+ for desc in klass.schema_inheritance_root_class.descendents
641
+ attrs.concat desc.serializable_attributes
642
+ end
643
+ end
644
+ return attrs.uniq
645
+ end
646
+
647
+ # Return the SQL table field for the given serializable
648
+ # attribute. You can override the default field name by
649
+ # annotating the attribute with a :field annotation.
650
+
651
+ def field_for_attribute(a, anno)
652
+ (f = anno[:field]) ? f : a
653
+ end
654
+
655
+ def field_sql_for_attribute(a, anno)
656
+ field = field_for_attribute(a, anno).to_s
657
+
658
+ if anno.sql?
659
+ field << " #{anno.sql}"
660
+ else
661
+ field << " #{sql_type_for_class(anno.class)}"
662
+ field << " UNIQUE" if anno.unique?
663
+ field << " DEFAULT #{quote(anno.default)} NOT NULL" if anno.default?
664
+ field << " #{anno.extra_sql}" if anno.extra_sql?
665
+ end
666
+
667
+ return field
668
+ end
669
+
670
+ # Create the fields that correspond to the class serializable
671
+ # attributes. The generated fields array is used in
672
+ # create_table.
673
+ #
674
+ # If the property has an :sql annotation this overrides the
675
+ # default mapping. If the property has an :extra_sql annotation
676
+ # the extra sql is appended after the default mapping.
677
+
678
+ def fields_for_class(klass)
679
+ fields = []
680
+ attrs = serializable_attributes_for_class(klass)
681
+
682
+ # FIXME: move to serializable attributes.
683
+
684
+ if klass.schema_inheritance?
685
+ # This class is a superclass in a single table inheritance
686
+ # chain. So inject a special class ogtype field that
687
+ # holds the class name.
688
+ fields << "ogtype VARCHAR(30)"
689
+ end
690
+
691
+ for a in attrs
692
+ anno = klass.ann[a]
693
+ if anno.null?
694
+ klass.subclasses.each do |subklass|
695
+ anno = subklass.ann[a] if anno.null?
696
+ end
697
+ end
698
+ fields << field_sql_for_attribute(a, anno)
699
+ end
700
+
701
+ return fields
702
+ end
703
+
704
+ # Returns the SQL indexed serializable attributes for the
705
+ # given class.
706
+
707
+ def sql_indices_for_class klass
708
+ indices = []
709
+
710
+ for a in klass.serializable_attributes
711
+ indices << a if klass.ann(a).index?
712
+ end
713
+
714
+ return indices
715
+ end
716
+
717
+ # Return the SQL type for the given Ruby class.
718
+
719
+ def sql_type_for_class klass
720
+ @typemap[klass]
721
+ end
722
+
723
+ # Generate code to serialize an attribute to an SQL table
724
+ # field.
725
+ # YAML is used (instead of Marshal) to store general Ruby
726
+ # objects to be more
727
+ # portable.
728
+ #
729
+ # === Input
730
+ #
731
+ # * s = attribute symbol
732
+ # * a = attribute annotations
733
+ #--
734
+ # No need to optimize this, used only to precalculate code.
735
+ # FIXME: add extra handling for float.
736
+ #++
737
+
738
+ def write_attr(s, a)
739
+ store = self.class
740
+
741
+ if a.class.ancestor? Integer
742
+ "#\{@#{s} || 'NULL'\}"
743
+
744
+ elsif a.class.ancestor? Float
745
+ "#\{@#{s} || 'NULL'\}"
746
+
747
+ elsif a.class.ancestor? String
748
+ %|#\{@#{s} ? "'#\{#{store}.escape(@#{s})\}'" : 'NULL'\}|
749
+
750
+ elsif a.class.ancestor? Time
751
+ %|#\{@#{s} ? "'#\{#{store}.timestamp(@#{s})\}'" : 'NULL'\}|
752
+
753
+ elsif a.class.ancestor? Date
754
+ %|#\{@#{s} ? "'#\{#{store}.date(@#{s})\}'" : 'NULL'\}|
755
+
756
+ elsif a.class.ancestor? TrueClass
757
+ "#\{@#{s} ? \"'t'\" : 'NULL' \}"
758
+
759
+ elsif a.class.ancestor? Og::Blob
760
+ %|#\{@#{s} ? "'#\{#{store}.escape(#{store}.blob(@#{s}))\}'" : 'NULL'\}|
761
+
762
+ else
763
+ # keep the '' for nil symbols.
764
+ %|#\{@#{s} ? "'#\{#{store}.escape(@#{s}.to_yaml)\}'" : "''"\}|
765
+ end
766
+ end
767
+
768
+ # Generate code to deserialize an SQL table field into an
769
+ # attribute.
770
+ #--
771
+ # No need to optimize this, used only to precalculate code.
772
+ #++
773
+
774
+ def read_attr(s, a, col)
775
+ store = self.class
776
+
777
+ if a.class.ancestor? Integer
778
+ "#{store}.parse_int(res[#{col} + offset])"
779
+
780
+ elsif a.class.ancestor? Float
781
+ "#{store}.parse_float(res[#{col} + offset])"
782
+
783
+ elsif a.class.ancestor? String
784
+ "res[#{col} + offset]"
785
+
786
+ elsif a.class.ancestor? Time
787
+ "#{store}.parse_timestamp(res[#{col} + offset])"
788
+
789
+ elsif a.class.ancestor? Date
790
+ "#{store}.parse_date(res[#{col} + offset])"
791
+
792
+ elsif a.class.ancestor? TrueClass
793
+ "#{store}.parse_boolean(res[#{col} + offset])"
794
+
795
+ elsif a.class.ancestor? Og::Blob
796
+ "#{store}.parse_blob(res[#{col} + offset])"
797
+
798
+ else
799
+ "YAML::load(res[#{col} + offset])"
800
+ end
801
+ end
802
+
803
+ # Get the fields from the database table. Also handles the
804
+ # change of ordering of the fields in the table.
805
+ #
806
+ # To ignore a database field use the ignore_fields annotation
807
+ # ie,
808
+ #
809
+ # class Article
810
+ # ann self, :ignore_fields => [ :tsearch_idx, :ext_field ]
811
+ # end
812
+ #
813
+ # other aliases for ignore_fiels: ignore_field, ignore_column.
814
+ #--
815
+ # Even though great care has been taken to make this
816
+ # method reusable, oveeride if needed in your adapter.
817
+ #++
818
+
819
+ def create_field_map(klass)
820
+ res = query "SELECT * FROM #{klass.table} LIMIT 1"
821
+ map = {}
822
+
823
+ # Check if the field should be ignored.
824
+
825
+ ignore = klass.ann.self[:ignore_field] || klass.ann.self[:ignore_fields] || klass.ann.self[:ignore_columns]
826
+
827
+ res.fields.each_with_index do |f, i|
828
+ field_name = f.to_sym
829
+ unless (ignore and ignore.include?(field_name))
830
+ map[field_name] = i
831
+ end
832
+ end
833
+
834
+ return map
835
+ ensure
836
+ res.close if res
837
+ end
838
+
839
+ # Generates the SQL field of the primary key for this class.
840
+
841
+ def pk_field klass
842
+ pk = klass.primary_key
843
+ return klass.ann(pk)[:field] || pk
844
+ end
845
+
846
+ # :section: Lifecycle method compilers.
847
+
848
+ # Compile the og_insert method for the class. This method is
849
+ # used to create insert the object into a new Row in the
850
+ # database.
851
+ #--
852
+ # Override the og_insert_kernel method to customize for
853
+ # adapters.
854
+ #++
855
+
856
+ def eval_og_insert(klass)
857
+ fields = []
858
+ values = []
859
+
860
+ for a in klass.serializable_attributes
861
+ anno = klass.ann(a)
862
+ fields << field_for_attribute(a, anno)
863
+ values << write_attr(a, anno)
864
+ end
865
+
866
+ # If the class participates in STI, automatically insert
867
+ # an ogtype serializable attribute.
868
+
869
+ if klass.schema_inheritance?
870
+ fields << :ogtype
871
+ values << quote(klass.name)
872
+ end
873
+
874
+ fields = fields.join(', ')
875
+ values = values.join(', ')
876
+
877
+ sql = "INSERT INTO #{klass.table} (#{fields}) VALUES (#{values})"
878
+
879
+ klass.module_eval %{
880
+ def og_insert(store)
881
+ #{insert_advices :pre, :og_insert, klass, :advices}
882
+ #{insert_sql sql, klass}
883
+ #{insert_advices :post, :og_insert, klass, :advices}
884
+ end
885
+ }
886
+ end
887
+
888
+ # The insert sql statements.
889
+
890
+ def insert_sql(sql, klass)
891
+ %{
892
+ store.exec "#{sql}"
893
+ @#{klass.primary_key} = store.last_insert_id
894
+ }
895
+ end
896
+
897
+ # Compile the og_update method for the class.
898
+
899
+ def eval_og_update(klass)
900
+ pk = klass.primary_key
901
+
902
+ updates = []
903
+
904
+ for a in klass.serializable_attributes.reject { |a| a == pk }
905
+ anno = klass.ann(a)
906
+ updates << "#{field_for_attribute a, anno}=#{write_attr a, anno}"
907
+ end
908
+
909
+ updates = updates.join(', ')
910
+
911
+ sql = "UPDATE #{klass.table} SET #{updates} WHERE #{pk_field klass}=#\{@#{pk}\}"
912
+
913
+ klass.module_eval %{
914
+ def og_update(store, options = nil)
915
+ #{insert_advices :pre, :og_update, klass, :advices}
916
+ sql = "#{sql}"
917
+ sql << " AND \#{options[:condition]}" if options and options[:condition]
918
+ changed = store.sql_update(sql)
919
+ #{insert_advices :post, :og_update, klass, :advices}
920
+ return changed
921
+ end
922
+ }
923
+ end
924
+
925
+ # Compile the og_read method for the class. This method is
926
+ # used to read (deserialize) the given class from the store.
927
+ # In order to allow for changing field/attribute orders a
928
+ # field mapping hash is used.
929
+
930
+ def eval_og_read(klass)
931
+ code = []
932
+
933
+ attrs = klass.attributes
934
+ field_map = create_field_map(klass)
935
+
936
+ for a in attrs
937
+ anno = klass.ann(a)
938
+
939
+ f = field_for_attribute(a, anno)
940
+
941
+ if col = field_map[f]
942
+ code << "@#{a} = #{read_attr a, anno, col}"
943
+ end
944
+ end
945
+
946
+ code = code.join('; ')
947
+
948
+ klass.module_eval %{
949
+ def og_read(res, row = 0, offset = 0)
950
+ #{insert_advices :pre, :og_read, klass, :advices}
951
+ #{code}
952
+ #{insert_advices :post, :og_read, klass, :advices}
953
+ end
954
+ }
955
+ end
956
+
957
+ # Compiles the og_delete method for this class. This method
958
+ # is used to delete instances of this class.
959
+
960
+ def eval_og_delete klass
961
+ klass.module_eval %{
962
+ def og_delete(store, cascade = true)
963
+ #{insert_advices :pre, :og_delete, klass, :advices}
964
+
965
+ if cascade
966
+
967
+ transaction do |tx|
968
+ tx.exec "DELETE FROM #{klass.table} WHERE #{pk_field klass}=\#{pk}"
969
+ if descendants = #{klass}.ann.self[:descendants]
970
+ descendants.each do |dclass, foreign_key|
971
+ tx.exec "DELETE FROM \#{dclass::OGTABLE} WHERE \#{foreign_key}=\#{pk}"
972
+ end
973
+ end
974
+ end
975
+
976
+ else
977
+ tx.exec "DELETE FROM #{klass.table} WHERE #{pk_field klass}=\#{pk}"
978
+ end
979
+
980
+ #{insert_advices :post, :og_delete, klass, :advices}
981
+ end
982
+ }
983
+ end
984
+
985
+ # Creates the schema for this class. Can be intercepted with
986
+ # aspects to add special behaviours.
987
+
988
+ def eval_og_create_schema(klass)
989
+ klass.module_eval %{
990
+ def og_create_schema(store)
991
+ if Og.create_schema
992
+ #{insert_advices :pre, :og_create_schema, klass, :advices}
993
+ unless self.class.schema_inheritance_child?
994
+ store.create_table #{klass}
995
+ end
996
+ store.evolve_schema(#{klass})
997
+ #{insert_advices :post, :og_create_schema, klass, :advices}
998
+ end
999
+ end
1000
+ }
1001
+ end
1002
+
1003
+ # Precompile a class specific allocate method. If this is an
1004
+ # STI parent classes it reads the class from the resultset.
1005
+
1006
+ def eval_og_allocate(klass)
1007
+ if klass.schema_inheritance?
1008
+ klass.module_eval %{
1009
+ def self.og_allocate(res, row = 0)
1010
+ Object.constant(res[0]).allocate
1011
+ end
1012
+ }
1013
+ else
1014
+ klass.module_eval %{
1015
+ def self.og_allocate(res, row = 0)
1016
+ self.allocate
1017
+ end
1018
+ }
1019
+ end
1020
+ end
1021
+
1022
+ # ...
1023
+
1024
+ def type_cast(klass, val)
1025
+ typemap = {
1026
+ Time => :parse_timestamp,
1027
+ Date => :parse_date,
1028
+ TrueClass => :parse_boolean
1029
+ }
1030
+
1031
+ if method = typemap[klass]
1032
+ send(method, val)
1033
+ else
1034
+ Integer(val) rescue Float(val) rescue raise "No conversion for #{klass} (#{val.inspect})"
1035
+ end
1036
+ end
1037
+
1084
1038
  # :section: Deserialization methods.
1085
1039
 
1086
1040
  # Read a field (column) from a result set row.
@@ -1102,16 +1056,48 @@ private
1102
1056
  # Deserialize the join relations.
1103
1057
 
1104
1058
  def read_join_relations(obj, res_row, row, join_relations)
1105
- offset = obj.class.properties.size
1059
+ offset = obj.class.serializable_attributes.size
1106
1060
 
1107
1061
  for rel in join_relations
1108
1062
  rel_obj = rel[:target_class].og_allocate(res_row, row)
1109
1063
  rel_obj.og_read(res_row, row, offset)
1110
- offset += rel_obj.class.properties.size
1064
+ offset += rel_obj.class.serializable_attributes.size
1111
1065
  obj.instance_variable_set("@#{rel[:name]}", rel_obj)
1112
1066
  end
1113
1067
  end
1114
1068
 
1069
+ # Deserialize one object from the ResultSet.
1070
+
1071
+ def read_one(res, klass, options = nil)
1072
+ return nil if res.blank?
1073
+
1074
+ if options and join_relations = options[:include]
1075
+ join_relations = [join_relations].flatten.collect do |n|
1076
+ klass.relation(n)
1077
+ end
1078
+ end
1079
+
1080
+ res_row = res.next
1081
+
1082
+ # causes STI classes to come back as the correct child class
1083
+ # if accessed from the superclass.
1084
+
1085
+ klass = Og::Entity::entity_from_string(res_row[0]) if klass.schema_inheritance?
1086
+ obj = klass.og_allocate(res_row, 0)
1087
+
1088
+ if options and options[:select]
1089
+ read_row(obj, res, res_row, 0)
1090
+ else
1091
+ obj.og_read(res_row)
1092
+ read_join_relations(obj, res_row, 0, join_relations) if join_relations
1093
+ end
1094
+
1095
+ return obj
1096
+
1097
+ ensure
1098
+ res.close
1099
+ end
1100
+
1115
1101
  # Deserialize all objects from the ResultSet.
1116
1102
 
1117
1103
  def read_all(res, klass, options = nil)
@@ -1156,13 +1142,24 @@ private
1156
1142
  end
1157
1143
  end
1158
1144
 
1159
- end
1145
+ # Returns true if a table exists within the database, false
1146
+ # otherwise.
1147
+
1148
+ def table_exists?(table)
1149
+ table_info(table) ? true : false
1150
+ end
1151
+ alias_method :table_exist?, :table_exists?
1152
+
1153
+ private
1160
1154
 
1155
+ def database_does_not_exist_exception?(ex)
1156
+ false
1157
+ end
1158
+
1159
+ def table_already_exists_exception?(ex)
1160
+ false
1161
+ end
1162
+
1161
1163
  end
1162
1164
 
1163
- # * George Moschovitis <gm@navel.gr>
1164
- # * Michael Neumann <mneumann@ntecs.de>
1165
- # * Ghislain Mary
1166
- # * Ysabel <deb@ysabel.org>
1167
- # * Guillaume Pierronnet <guillaume.pierronnet@laposte.net>
1168
- # * Rob Pitt <rob@motionpath.com>
1165
+ end