og 0.31.0 → 0.40.0

Sign up to get free protection for your applications and to get access to all the features.
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