og 0.16.0 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/CHANGELOG +485 -0
  2. data/README +35 -12
  3. data/Rakefile +4 -7
  4. data/benchmark/bench.rb +1 -1
  5. data/doc/AUTHORS +3 -3
  6. data/doc/RELEASES +153 -2
  7. data/doc/config.txt +0 -7
  8. data/doc/tutorial.txt +7 -0
  9. data/examples/README +5 -0
  10. data/examples/mysql_to_psql.rb +25 -50
  11. data/examples/run.rb +62 -77
  12. data/install.rb +1 -1
  13. data/lib/og.rb +45 -106
  14. data/lib/og/collection.rb +156 -0
  15. data/lib/og/entity.rb +131 -0
  16. data/lib/og/errors.rb +10 -15
  17. data/lib/og/manager.rb +115 -0
  18. data/lib/og/{mixins → mixin}/hierarchical.rb +43 -37
  19. data/lib/og/{mixins → mixin}/orderable.rb +35 -35
  20. data/lib/og/{mixins → mixin}/timestamped.rb +0 -6
  21. data/lib/og/{mixins → mixin}/tree.rb +0 -4
  22. data/lib/og/relation.rb +178 -0
  23. data/lib/og/relation/belongs_to.rb +14 -0
  24. data/lib/og/relation/has_many.rb +62 -0
  25. data/lib/og/relation/has_one.rb +17 -0
  26. data/lib/og/relation/joins_many.rb +69 -0
  27. data/lib/og/relation/many_to_many.rb +17 -0
  28. data/lib/og/relation/refers_to.rb +31 -0
  29. data/lib/og/store.rb +223 -0
  30. data/lib/og/store/filesys.rb +113 -0
  31. data/lib/og/store/madeleine.rb +4 -0
  32. data/lib/og/store/memory.rb +291 -0
  33. data/lib/og/store/mysql.rb +283 -0
  34. data/lib/og/store/psql.rb +238 -0
  35. data/lib/og/store/sql.rb +599 -0
  36. data/lib/og/store/sqlite.rb +190 -0
  37. data/lib/og/store/sqlserver.rb +262 -0
  38. data/lib/og/types.rb +19 -0
  39. data/lib/og/validation.rb +0 -4
  40. data/test/og/{mixins → mixin}/tc_hierarchical.rb +21 -23
  41. data/test/og/{mixins → mixin}/tc_orderable.rb +15 -14
  42. data/test/og/mixin/tc_timestamped.rb +38 -0
  43. data/test/og/store/tc_filesys.rb +71 -0
  44. data/test/og/tc_relation.rb +36 -0
  45. data/test/og/tc_store.rb +290 -0
  46. data/test/og/tc_types.rb +21 -0
  47. metadata +54 -40
  48. data/examples/mock_example.rb +0 -50
  49. data/lib/og/adapters/base.rb +0 -706
  50. data/lib/og/adapters/filesys.rb +0 -117
  51. data/lib/og/adapters/mysql.rb +0 -350
  52. data/lib/og/adapters/oracle.rb +0 -368
  53. data/lib/og/adapters/psql.rb +0 -272
  54. data/lib/og/adapters/sqlite.rb +0 -265
  55. data/lib/og/adapters/sqlserver.rb +0 -356
  56. data/lib/og/database.rb +0 -290
  57. data/lib/og/enchant.rb +0 -149
  58. data/lib/og/meta.rb +0 -407
  59. data/lib/og/testing/mock.rb +0 -165
  60. data/lib/og/typemacros.rb +0 -24
  61. data/test/og/adapters/tc_filesys.rb +0 -83
  62. data/test/og/adapters/tc_sqlite.rb +0 -86
  63. data/test/og/adapters/tc_sqlserver.rb +0 -96
  64. data/test/og/tc_automanage.rb +0 -46
  65. data/test/og/tc_lifecycle.rb +0 -105
  66. data/test/og/tc_many_to_many.rb +0 -61
  67. data/test/og/tc_meta.rb +0 -55
  68. data/test/og/tc_validation.rb +0 -89
  69. data/test/tc_og.rb +0 -364
@@ -0,0 +1,599 @@
1
+ require 'yaml'
2
+
3
+ module Og
4
+
5
+ module SqlUtils
6
+
7
+ # Escape an SQL string
8
+
9
+ def escape(str)
10
+ return nil unless str
11
+ return str.gsub(/'/, "''")
12
+ end
13
+
14
+ # Convert a ruby time to an sql timestamp.
15
+ #--
16
+ # TODO: Optimize this
17
+ #++
18
+
19
+ def timestamp(time = Time.now)
20
+ return nil unless time
21
+ return time.strftime("%Y-%m-%d %H:%M:%S")
22
+ end
23
+
24
+ # Output YYY-mm-dd
25
+ #--
26
+ # TODO: Optimize this.
27
+ #++
28
+
29
+ def date(date)
30
+ return nil unless date
31
+ return "#{date.year}-#{date.month}-#{date.mday}"
32
+ end
33
+
34
+ # Parse sql datetime
35
+ #--
36
+ # TODO: Optimize this.
37
+ #++
38
+
39
+ def parse_timestamp(str)
40
+ return nil unless str
41
+ return Time.parse(str)
42
+ end
43
+
44
+ # Input YYYY-mm-dd
45
+ #--
46
+ # TODO: Optimize this.
47
+ #++
48
+
49
+ def parse_date(str)
50
+ return nil unless str
51
+ return Date.strptime(str)
52
+ end
53
+
54
+ def quote(val)
55
+ case val
56
+ when Fixnum, Integer, Float
57
+ val ? val.to_s : 'NULL'
58
+ when String
59
+ val ? "'#{escape(val)}'" : 'NULL'
60
+ when Time
61
+ val ? "'#{timestamp(val)}'" : 'NULL'
62
+ when Date
63
+ val ? "'#{date(val)}'" : 'NULL'
64
+ when TrueClass
65
+ val ? "'t'" : 'NULL'
66
+ else
67
+ # gmosx: keep the '' for nil symbols.
68
+ val ? escape(val.to_yaml) : ''
69
+ end
70
+ end
71
+
72
+ def table(klass)
73
+ "#{Og.table_prefix}#{klass.to_s.gsub(/::/, "_").downcase}"
74
+ end
75
+
76
+ def join_table(class1, class2, postfix = nil)
77
+ if class1.to_s < class2.to_s
78
+ return "j#{table(class1)}#{table(class2)}#{postfix}", 1, 2
79
+ else
80
+ return "j#{table(class2)}#{table(class1)}#{postfix}", 2, 1
81
+ end
82
+ end
83
+ end
84
+
85
+ # A Store that persists objects into a PostgreSQL database.
86
+
87
+ class SqlStore < Store
88
+ extend SqlUtils
89
+ include SqlUtils
90
+
91
+ # The connection to the backend SQL RDBMS.
92
+
93
+ attr_accessor :conn
94
+
95
+ def initialize(options)
96
+ super
97
+
98
+ # The default Ruby <-> SQL type mappings, should be valid for most
99
+ # RDBM systems.
100
+
101
+ @typemap = {
102
+ Integer => 'integer',
103
+ Fixnum => 'integer',
104
+ Float => 'float',
105
+ String => 'text',
106
+ Time => 'timestamp',
107
+ Date => 'date',
108
+ TrueClass => 'boolean',
109
+ Object => 'text',
110
+ Array => 'text',
111
+ Hash => 'text'
112
+ }
113
+ end
114
+
115
+ #--
116
+ # FIXME: not working.
117
+ #++
118
+
119
+ def enable_logging
120
+ require 'glue/aspects'
121
+ klass = self.class
122
+ klass.send :include, Glue::Aspects
123
+ klass.pre "Logger.info sql", :on => [:exec, :query]
124
+ Glue::Aspects.wrap(klass, [:exec, :query])
125
+ end
126
+
127
+ # Enchants a class.
128
+
129
+ def enchant(klass, manager)
130
+ klass.const_set 'OGTABLE', table(klass)
131
+ klass.module_eval 'def self.table; OGTABLE; end'
132
+
133
+ super
134
+
135
+ create_table(klass) if Og.create_schema
136
+
137
+ eval_og_insert(klass)
138
+ eval_og_update(klass)
139
+ eval_og_read(klass)
140
+ eval_og_delete(klass)
141
+ end
142
+
143
+ # :section: Lifecycle methods.
144
+
145
+ # Loads an object from the store using the primary key.
146
+
147
+ def load(pk, klass)
148
+ res = query "SELECT * FROM #{klass::OGTABLE} WHERE #{klass.pk_symbol}=#{pk}"
149
+ read_one(res, klass)
150
+ end
151
+
152
+ # Reloads an object from the store.
153
+
154
+ def reload(obj, pk)
155
+ raise 'Cannot reload unmanaged object' unless obj.saved?
156
+ res = query "SELECT * FROM #{obj.class.table} WHERE #{obj.class.pk_symbol}=#{pk}"
157
+ obj.og_read(res.next, 0)
158
+ ensure
159
+ res.close if res
160
+ end
161
+
162
+ # If a properties collection is provided, only updates the
163
+ # selected properties. Pass the required properties as symbols
164
+ # or strings.
165
+
166
+ def update(obj, properties = nil)
167
+ if properties
168
+ if properties.is_a?(Array)
169
+ set = []
170
+ for p in properties
171
+ set << "#{p}=#{quote(obj.send(p))}"
172
+ end
173
+ set = set.join(',')
174
+ else
175
+ set = "#{properties}=#{quote(obj.send(properties))}"
176
+ end
177
+ exec "UPDATE #{obj.class.table} SET #{set} WHERE #{obj.class.pk_symbol}=#{obj.pk}"
178
+ else
179
+ obj.og_update(self)
180
+ end
181
+ end
182
+
183
+ # Update selected properties of an object or class of
184
+ # objects.
185
+
186
+ def update_properties(target, set, options = nil)
187
+ set = set.gsub(/@/, '')
188
+
189
+ if target.is_a?(Class)
190
+ sql = "UPDATE #{target.table} SET #{set} "
191
+ if options
192
+ if condition = options[:condition] || options[:where]
193
+ sql << " WHERE #{condition}"
194
+ end
195
+ end
196
+ exec sql
197
+ else
198
+ exec "UPDATE #{target.class.table} SET #{set} WHERE #{target.class.pk_symbol}=#{target.pk}"
199
+ end
200
+ end
201
+ alias_method :pupdate, :update_properties
202
+ alias_method :update_property, :update_properties
203
+
204
+ # Find a collection of objects.
205
+ #
206
+ # === Examples
207
+ #
208
+ # User.find(:condition => 'age > 15', :order => 'score ASC', :offet => 10, :limit =>10)
209
+ # Comment.find(:include => :entry)
210
+
211
+ def find(options)
212
+ klass = options[:class]
213
+ sql = resolve_options(klass, options)
214
+ read_all(query(sql), klass, options[:include])
215
+ end
216
+
217
+ # Find one object.
218
+
219
+ def find_one(options)
220
+ klass = options[:class]
221
+ options[:limit] ||= 1
222
+ sql = resolve_options(klass, options)
223
+ read_one(query(sql), klass, options[:include])
224
+ end
225
+
226
+ def count(options)
227
+ if options.is_a?(String)
228
+ sql = options
229
+ else
230
+ sql = "SELECT COUNT(*) FROM #{options[:class]::OGTABLE}"
231
+ if condition = options[:condition]
232
+ sql << " WHERE #{condition}"
233
+ end
234
+ end
235
+
236
+ query(sql).first_value.to_i
237
+ end
238
+
239
+ # Relate two objects through an intermediate join table.
240
+ # Typically used in joins_many and many_to_many relations.
241
+
242
+ def join(obj1, obj2, table)
243
+ if obj1.class.to_s > obj2.class.to_s
244
+ obj1, obj2 = obj2, obj1
245
+ end
246
+
247
+ exec "INSERT INTO #{table} (key1, key2) VALUES (#{obj1.pk}, #{obj2.pk})"
248
+ end
249
+
250
+ # :section: Transaction methods.
251
+
252
+ # Start a new transaction.
253
+
254
+ def start
255
+ exec('START TRANSACTION') if @transaction_nesting < 1
256
+ @transaction_nesting += 1
257
+ end
258
+
259
+ # Commit a transaction.
260
+
261
+ def commit
262
+ @transaction_nesting -= 1
263
+ exec('COMMIT') if @transaction_nesting < 1
264
+ end
265
+
266
+ # Rollback a transaction.
267
+
268
+ def rollback
269
+ @transaction_nesting -= 1
270
+ exec('ROLLBACK') if @transaction_nesting < 1
271
+ end
272
+
273
+ private
274
+
275
+ def create_table(klass)
276
+ raise 'Not implemented'
277
+ end
278
+
279
+ def drop_table(klass)
280
+ exec "DROP TABLE #{klass.table}"
281
+ end
282
+
283
+ # Create the columns that correpsond to the klass properties.
284
+ # The generated columns array is used in create_table.
285
+ # If the property has an :sql metadata this overrides the
286
+ # default mapping. If the property has an :extra_sql metadata
287
+ # the extra sql is appended after the default mapping.
288
+
289
+ def columns_for_class(klass)
290
+ columns = []
291
+
292
+ klass.__props.each do |p|
293
+ klass.index(p.symbol) if p.meta[:index]
294
+
295
+ column = p.symbol.to_s
296
+
297
+ if p.meta and p.meta[:sql]
298
+ column << " #{p.meta[:sql]}"
299
+ else
300
+ column << " #{type_for_class(p.klass)}"
301
+
302
+ if p.meta
303
+ if default = p.meta[:default]
304
+ column << " DEFAULT #{default.inspect} NOT NULL"
305
+ end
306
+
307
+ column << " UNIQUE" if p.meta[:unique]
308
+
309
+ if extra_sql = p.meta[:extra_sql]
310
+ column << " #{extra_sql}"
311
+ end
312
+ end
313
+ end
314
+
315
+ columns << column
316
+ end
317
+
318
+ return columns
319
+ end
320
+
321
+ def type_for_class(klass)
322
+ @typemap[klass]
323
+ end
324
+
325
+ # Return an sql string evaluator for the property.
326
+ # No need to optimize this, used only to precalculate code.
327
+ # YAML is used to store general Ruby objects to be more
328
+ # portable.
329
+ #--
330
+ # FIXME: add extra handling for float.
331
+ #++
332
+
333
+ def write_prop(p)
334
+ if p.klass.ancestors.include?(Integer)
335
+ return "#\{@#{p.symbol} || 'NULL'\}"
336
+ elsif p.klass.ancestors.include?(Float)
337
+ return "#\{@#{p.symbol} || 'NULL'\}"
338
+ elsif p.klass.ancestors.include?(String)
339
+ return %|#\{@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol})\}'" : 'NULL'\}|
340
+ elsif p.klass.ancestors.include?(Time)
341
+ return %|#\{@#{p.symbol} ? "'#\{#{self.class}.timestamp(@#{p.symbol})\}'" : 'NULL'\}|
342
+ elsif p.klass.ancestors.include?(Date)
343
+ return %|#\{@#{p.symbol} ? "'#\{#{self.class}.date(@#{p.symbol})\}'" : 'NULL'\}|
344
+ elsif p.klass.ancestors.include?(TrueClass)
345
+ return "#\{@#{p.symbol} ? \"'t'\" : 'NULL' \}"
346
+ else
347
+ # gmosx: keep the '' for nil symbols.
348
+ return %|#\{@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol}.to_yaml)\}'" : "''"\}|
349
+ end
350
+ end
351
+
352
+ # Return an evaluator for reading the property.
353
+ # No need to optimize this, used only to precalculate code.
354
+
355
+ def read_prop(p, col)
356
+ if p.klass.ancestors.include?(Integer)
357
+ return "res[#{col} + offset].to_i"
358
+ elsif p.klass.ancestors.include?(Float)
359
+ return "res[#{col} + offset].to_f"
360
+ elsif p.klass.ancestors.include?(String)
361
+ return "res[#{col} + offset]"
362
+ elsif p.klass.ancestors.include?(Time)
363
+ return "#{self.class}.parse_timestamp(res[#{col} + offset])"
364
+ elsif p.klass.ancestors.include?(Date)
365
+ return "#{self.class}.parse_date(res[#{col} + offset])"
366
+ elsif p.klass.ancestors.include?(TrueClass)
367
+ return "('0' != res[#{col} + offset])"
368
+ else
369
+ return "YAML::load(res[#{col} + offset])"
370
+ end
371
+ end
372
+
373
+ # :section: Lifecycle method compilers.
374
+
375
+ # Compile the og_update method for the class.
376
+
377
+ def eval_og_insert(klass)
378
+ pk = klass.pk_symbol
379
+ props = klass.properties
380
+ values = props.collect { |p| write_prop(p) }.join(',')
381
+
382
+ sql = "INSERT INTO #{klass.table} (#{props.collect {|p| p.symbol.to_s}.join(',')}) VALUES (#{values})"
383
+
384
+ klass.module_eval %{
385
+ def og_insert(store)
386
+ #{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
387
+ store.exec "#{sql}"
388
+ #{Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
389
+ end
390
+ }
391
+ end
392
+
393
+ # Compile the og_update method for the class.
394
+
395
+ def eval_og_update(klass)
396
+ pk = klass.pk_symbol
397
+ props = klass.properties.reject { |p| pk == p.symbol }
398
+
399
+ updates = props.collect { |p|
400
+ "#{p.symbol}=#{write_prop(p)}"
401
+ }
402
+
403
+ sql = "UPDATE #{klass::OGTABLE} SET #{updates.join(', ')} WHERE #{pk}=#\{@#{pk}\}"
404
+
405
+ klass.module_eval %{
406
+ def og_update(store)
407
+ #{Aspects.gen_advice_code(:og_update, klass.advices, :pre) if klass.respond_to?(:advices)}
408
+ store.exec "#{sql}"
409
+ #{Aspects.gen_advice_code(:og_update, klass.advices, :post) if klass.respond_to?(:advices)}
410
+ end
411
+ }
412
+ end
413
+
414
+ # Compile the og_read method for the class. This method is
415
+ # used to read (deserialize) the given class from the store.
416
+ # In order to allow for changing column/attribute orders a
417
+ # column mapping hash is used.
418
+
419
+ def eval_og_read(klass)
420
+ code = []
421
+ props = klass.properties
422
+ column_map = create_column_map(klass)
423
+
424
+ props.each do |p|
425
+ if col = column_map[p.symbol]
426
+ code << "@#{p.symbol} = #{read_prop(p, col)}"
427
+ end
428
+ end
429
+
430
+ code = code.join('; ')
431
+
432
+ klass.module_eval %{
433
+ def og_read(res, row = 0, offset = 0)
434
+ #{Aspects.gen_advice_code(:og_read, klass.advices, :pre) if klass.respond_to?(:advices)}
435
+ #{code}
436
+ #{Aspects.gen_advice_code(:og_read, klass.advices, :post) if klass.respond_to?(:advices)}
437
+ end
438
+ }
439
+ end
440
+
441
+ #--
442
+ # FIXME: is pk needed as parameter?
443
+ #++
444
+
445
+ def eval_og_delete(klass)
446
+ klass.module_eval %{
447
+ def og_delete(store, pk, cascade = true)
448
+ #{Aspects.gen_advice_code(:og_delete, klass.advices, :pre) if klass.respond_to?(:advices)}
449
+ pk ||= @#{klass.pk_symbol}
450
+ transaction do |tx|
451
+ tx.exec "DELETE FROM #{klass.table} WHERE #{klass.pk_symbol}=\#{pk}"
452
+ if cascade and #{klass}.__meta[:descendants]
453
+ #{klass}.__meta[:descendants].each do |dclass, foreign_key|
454
+ tx.exec "DELETE FROM \#{dclass::OGTABLE} WHERE \#{foreign_key}=\#{pk}"
455
+ end
456
+ end
457
+ end
458
+ #{Aspects.gen_advice_code(:og_delete, klass.advices, :post) if klass.respond_to?(:advices)}
459
+ end
460
+ }
461
+ end
462
+
463
+ # :section: Misc methods.
464
+
465
+ def handle_sql_exception(ex, sql = nil)
466
+ Logger.error "DB error #{ex}, [#{sql}]"
467
+ Logger.error ex.backtrace.join("\n")
468
+ raise StoreException.new(ex, sql) if Og.raise_store_exceptions
469
+
470
+ # FIXME: should return :error or something.
471
+ return nil
472
+ end
473
+
474
+ def resolve_options(klass, options)
475
+ tables = [klass::OGTABLE]
476
+
477
+ if included = options[:include]
478
+ join_conditions = []
479
+
480
+ for name in [included].flatten
481
+ if rel = klass.relation(name)
482
+ target_table = rel[:target_class]::OGTABLE
483
+ tables << target_table
484
+ join_conditions << "#{klass::OGTABLE}.#{rel[:foreign_key]}=#{target_table}.#{rel[:target_pk]}"
485
+ else
486
+ raise 'Unknown relation name'
487
+ end
488
+ end
489
+
490
+ columns = tables.collect { |t| "#{t}.*" }.join(',')
491
+
492
+ if options[:condition]
493
+ options[:condition] += " AND #{join_conditions.join(' AND ')}"
494
+ else
495
+ options[:condition] = join_conditions.join(' AND ')
496
+ end
497
+ else
498
+ columns = '*'
499
+ end
500
+
501
+ if join_table = options[:join_table]
502
+ tables << join_table
503
+ if options[:condition]
504
+ options[:condition] += " AND #{options[:join_condition]}"
505
+ else
506
+ options[:condition] = options[:join_condition]
507
+ end
508
+ end
509
+
510
+ sql = "SELECT #{columns} FROM #{tables.join(',')}"
511
+
512
+ if condition = options[:condition] || options[:where]
513
+ sql << " WHERE #{condition}"
514
+ end
515
+
516
+ if order = options[:order]
517
+ sql << " ORDER BY #{order}"
518
+ end
519
+
520
+ if offset = options[:offset]
521
+ sql << " OFFSET #{offset}"
522
+ end
523
+
524
+ if limit = options[:limit]
525
+ sql << " LIMIT #{limit}"
526
+ end
527
+
528
+ if extra = options[:extra]
529
+ sql << " #{extra}"
530
+ end
531
+
532
+ return sql
533
+ end
534
+
535
+ # :section: Deserialization methods.
536
+
537
+ # Deserialize the join relations.
538
+
539
+ def read_join_relations(obj, res, row, join_relations)
540
+ offset = obj.class.properties.size
541
+
542
+ for rel in join_relations
543
+ rel_obj = rel[:target_class].allocate
544
+ rel_obj.og_read(res, row, offset)
545
+ offset += rel_obj.class.properties.size
546
+ obj.instance_variable_set("@#{rel[:name]}", rel_obj)
547
+ end
548
+ end
549
+
550
+ # Deserialize one object from the ResultSet.
551
+
552
+ def read_one(res, klass, join_relations = nil)
553
+ return nil if res.blank?
554
+
555
+ if join_relations
556
+ join_relations = [join_relations].flatten.collect do |n|
557
+ klass.relation(n)
558
+ end
559
+ end
560
+
561
+ row = res.next
562
+
563
+ obj = klass.allocate
564
+ obj.og_read(row)
565
+ read_join_relations(obj, row, 0, join_relations) if join_relations
566
+
567
+ res.close
568
+
569
+ return obj
570
+ end
571
+
572
+ # Deserialize all objects from the ResultSet.
573
+
574
+ def read_all(res, klass, join_relations)
575
+ return [] if res.blank?
576
+
577
+ if join_relations
578
+ join_relations = [join_relations].flatten.collect do |n|
579
+ klass.relation(n)
580
+ end
581
+ end
582
+
583
+ objects = []
584
+
585
+ res.each_row do |res_row, row|
586
+ obj = klass.allocate
587
+ obj.og_read(res_row, row)
588
+ read_join_relations(obj, res_row, row, join_relations) if join_relations
589
+ objects << obj
590
+ end
591
+
592
+ res.close
593
+
594
+ return objects
595
+ end
596
+
597
+ end
598
+
599
+ end