og 0.16.0 → 0.17.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 (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