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,17 @@
1
+ require 'og/relation/joins_many'
2
+
3
+ module Og
4
+
5
+ # A 'many_to_many' relation.
6
+ # This objects is associated with an other using an intermediate
7
+ # join table. Just an alias for 'joins_many'.
8
+ #
9
+ # === Examples
10
+ #
11
+ # many_to_many Category
12
+ # many_to_many :categories, Category
13
+
14
+ class ManyToMany < JoinsMany
15
+ end
16
+
17
+ end
@@ -0,0 +1,31 @@
1
+ require 'og/relation'
2
+
3
+ module Og
4
+
5
+ class RefersTo < Relation
6
+
7
+ def enchant
8
+ self[:foreign_key] = "#{foreign_name || target_singular_name}_#{target_pk}"
9
+
10
+ owner_class.module_eval %{
11
+ attr_accessor :#{target_singular_name}
12
+ prop_accessor :#{foreign_key}, #{target_pkclass}
13
+
14
+ def #{target_singular_name}(reload = false)
15
+ unless reload
16
+ @#{target_singular_name} = #{target_class}[@#{foreign_key}] unless @#{target_singular_name}
17
+ return @#{target_singular_name}
18
+ else
19
+ return #{target_class}[@#{foreign_key}]
20
+ end
21
+ end
22
+
23
+ def #{target_singular_name}=(obj)
24
+ @#{foreign_key} = obj.#{target_pk}
25
+ end
26
+ }
27
+ end
28
+
29
+ end
30
+
31
+ end
data/lib/og/store.rb ADDED
@@ -0,0 +1,223 @@
1
+ module Og
2
+
3
+ # A Store is responsible for the peristance of the ObjectGraph.
4
+
5
+ class Store
6
+
7
+ # Options.
8
+
9
+ attr_accessor :options
10
+
11
+ # Transaction nesting.
12
+
13
+ attr_accessor :transaction_nesting
14
+
15
+ # :section: Store methods.
16
+
17
+ # Return the store for the given name.
18
+
19
+ def self.for_name(name)
20
+ # gmosx: to keep RDoc happy.
21
+ eval %{
22
+ require 'og/store/#{name}'
23
+ return #{name.to_s.capitalize}Store
24
+ }
25
+ end
26
+
27
+ # Creates a store.
28
+
29
+ def self.create(options)
30
+ end
31
+
32
+ # Destroys a store.
33
+
34
+ def self.destroy(options)
35
+ end
36
+
37
+ # :section: Misc methods.
38
+
39
+ # Create a session to the store.
40
+
41
+ def initialize(options)
42
+ @options = options
43
+ @transaction_nesting = 0
44
+ end
45
+
46
+ # Close the session to the store.
47
+
48
+ def close
49
+ end
50
+
51
+ # Enchants a class.
52
+
53
+ def enchant(klass, manager)
54
+ klass.class.send(:define_method, :index) do |arg|
55
+ meta :index, arg
56
+ end
57
+
58
+ pk = klass.primary_key.first
59
+
60
+ klass.module_eval %{
61
+ def saved?
62
+ return @#{klass.primary_key.first}
63
+ end
64
+
65
+ def unsaved?
66
+ return !@#{klass.primary_key.first}
67
+ end
68
+
69
+ # Evaluate an alias for the primary key.
70
+
71
+ alias_method :pk, :#{pk}
72
+ alias_method :pk=, :#{pk}=
73
+
74
+ def self.pk_symbol
75
+ :#{klass.primary_key.first}
76
+ end
77
+ }
78
+
79
+ # Generate finder methods.
80
+
81
+ code = ''
82
+
83
+ for p in klass.properties
84
+ finder = p.meta[:unique] ? 'find_one' : 'find'
85
+
86
+ code << %{
87
+ def self.find_by_#{p.symbol}(val, operator = '=', options = {})
88
+ options.update(
89
+ :class => #{klass},
90
+ :condition => "#{p.symbol}\#{operator}\#{ogmanager.store.quote(val)}"
91
+ )
92
+ ogmanager.store.#{finder}(options)
93
+ end;
94
+ }
95
+ end
96
+
97
+ klass.module_eval(code)
98
+ end
99
+
100
+ # :section: Lifecycle methods.
101
+
102
+ # Loads an object from the store using the primary key.
103
+
104
+ def load(pk, klass)
105
+ end
106
+
107
+ # Reloads an object from the store.
108
+
109
+ def reload(obj)
110
+ end
111
+
112
+ # Save an object to store. Insert if this is a new object or
113
+ # update if this is already inserted in the database.
114
+
115
+ def save(obj)
116
+ if obj.saved?
117
+ obj.og_update(self)
118
+ else
119
+ obj.og_insert(self)
120
+ end
121
+ end
122
+ alias_method :<<, :save
123
+
124
+ # Insert an object in the store.
125
+
126
+ def insert(obj)
127
+ obj.og_insert(self)
128
+ end
129
+
130
+ # Update an object in the store.
131
+
132
+ def update(obj, properties = nil)
133
+ obj.og_update(self)
134
+ end
135
+
136
+ # Update selected properties of an object or class of
137
+ # objects.
138
+
139
+ def update_properties(obj_or_class, props, options = nil)
140
+ end
141
+ alias_method :pupdate, :update_properties
142
+ alias_method :update_property, :update_properties
143
+
144
+ # Permanently delete an object from the store.
145
+
146
+ def delete(obj_or_pk, klass = nil, cascade = true)
147
+ unless obj_or_pk.is_a?(EntityMixin)
148
+ # create a dummy instance to keep the og_delete
149
+ # method as an instance method like the other lifecycle
150
+ # methods.
151
+ klass.allocate.og_delete(self, obj_or_pk, cascade)
152
+ else
153
+ obj_or_pk.og_delete(self, nil, cascade)
154
+ end
155
+ end
156
+
157
+ # Perform a query.
158
+
159
+ def find(klass, options)
160
+ end
161
+
162
+ # Count the results returned by the query.
163
+
164
+ def count(options)
165
+ end
166
+
167
+ # :section: Transaction methods.
168
+
169
+ # Start a new transaction.
170
+
171
+ def start
172
+ raise 'Not implemented'
173
+ true if @transaction_nesting < 1
174
+ @transaction_nesting += 1
175
+ end
176
+
177
+ # Commit a transaction.
178
+
179
+ def commit
180
+ raise 'Not implemented'
181
+ @transaction_nesting -= 1
182
+ true if @transaction_nesting < 1
183
+ end
184
+
185
+ # Rollback a transaction.
186
+
187
+ def rollback
188
+ @transaction_nesting -= 1
189
+ true if @transaction_nesting < 1
190
+ end
191
+
192
+ # Transaction helper. In the transaction block use
193
+ # the db pointer to the backend.
194
+
195
+ def transaction(&block)
196
+ begin
197
+ start
198
+ yield(self)
199
+ commit
200
+ rescue => ex
201
+ Logger.error 'Error in transaction'
202
+ Logger.error ex
203
+ Logger.error ex.backtrace
204
+ rollback
205
+ end
206
+ end
207
+
208
+ private
209
+
210
+ def eval_og_insert(klass)
211
+ end
212
+
213
+ def eval_og_update(klass)
214
+ end
215
+
216
+ def eval_og_read(klass)
217
+ end
218
+
219
+ def eval_og_delete(klass)
220
+ end
221
+ end
222
+
223
+ end
@@ -0,0 +1,113 @@
1
+ require 'yaml'
2
+ require 'fileutils'
3
+
4
+ require 'og/store'
5
+
6
+ module Og
7
+
8
+ # A Store that saves the object on the Filesystem.
9
+ # You can create multiple instances that point to the same store.
10
+
11
+ class FilesysStore < Store
12
+
13
+ # :section: Store methods.
14
+
15
+ def self.dir(name)
16
+ "#{name}_db"
17
+ end
18
+
19
+ def self.create(options)
20
+ name = dir(options[:name])
21
+ FileUtils.mkdir_p(name)
22
+ rescue
23
+ Logger.error "Cannot create '#{name}'"
24
+ end
25
+
26
+ def self.destroy(options)
27
+ name = dir(options[:name])
28
+ FileUtils.rm_rf(name)
29
+ rescue
30
+ Logger.error "Cannot destroy '#{name}'"
31
+ end
32
+
33
+ # :section: Misc methods.
34
+
35
+ # Initialize a connection to this store.
36
+
37
+ def initialize(options)
38
+ super
39
+
40
+ @base_dir = self.class.dir(options[:name])
41
+
42
+ unless File.directory?(@base_dir)
43
+ Logger.info "Database '#{options[:name]}' not found!"
44
+ self.class.create(options)
45
+ end
46
+ end
47
+
48
+ def enchant(klass, manager)
49
+ super
50
+
51
+ klass.const_set 'OGNAME', klass.to_s.gsub(/::/, "/").downcase
52
+ klass.property :oid, Fixnum
53
+
54
+ FileUtils.mkdir_p(path(klass))
55
+ File.open(spath(klass), 'w') { |f| f << '1' }
56
+ rescue => ex
57
+ p ex
58
+ Logger.error "Cannot create directory to store '#{klass}' classes!"
59
+ end
60
+
61
+ # :section: Lifecycle methods.
62
+
63
+ def load(oid, klass)
64
+ obj = nil
65
+ File.open(path(klass, oid), 'r') { |f| obj = YAML::load(f.read) }
66
+ return obj
67
+ rescue
68
+ return nil
69
+ end
70
+
71
+ def reload(obj)
72
+ end
73
+
74
+ def save(obj)
75
+ seq = nil
76
+ File.open(spath(obj.class), 'r') { |f| seq = f.read.to_i }
77
+ obj.oid = seq
78
+ File.open(path(obj.class, obj.oid), 'w') { |f| f << obj.to_yaml }
79
+ seq += 1
80
+ File.open(spath(obj.class), 'w') { |f| f << seq }
81
+ end
82
+
83
+ def delete(obj_or_pk, klass = nil, cascade = true)
84
+ pk = obj_or_pk.is_a?(Fixnum) ? obj_or_pk : obj_or_pk.oid
85
+ klass ||= obj_or_pk.class
86
+ FileUtils.rm(path(klass, pk))
87
+ end
88
+
89
+ def find
90
+ end
91
+
92
+ def count
93
+ end
94
+
95
+ private
96
+
97
+ # Path helper.
98
+
99
+ def path(klass, pk = nil)
100
+ class_dir = File.join(@base_dir, klass::OGNAME)
101
+ pk ? File.join(class_dir, "#{pk}.yml") : class_dir
102
+ end
103
+
104
+ # Path to sequence helper.
105
+
106
+ def spath(klass)
107
+ File.join(path(klass), 'seq')
108
+ end
109
+
110
+ end
111
+
112
+ end
113
+
@@ -0,0 +1,4 @@
1
+ # $Id$
2
+
3
+ module Og
4
+ end
@@ -0,0 +1,291 @@
1
+ require 'yaml'
2
+ require 'fileutils'
3
+
4
+ module Og
5
+
6
+ # A collection of utilities. Mainly to stay compatible with the
7
+ # SQL based backends.
8
+
9
+ module MemoryUtils
10
+
11
+ # FIXME: find a neutral name.
12
+
13
+ def table(klass)
14
+ klass.to_s
15
+ end
16
+
17
+ # FIXME: find a neutral name.
18
+
19
+ def join_table(class1, class2, postfix = nil)
20
+ "#{class1}#{class2}"
21
+ end
22
+
23
+ def quote(val)
24
+ val.inspect
25
+ end
26
+
27
+ end
28
+
29
+ # A Store that 'persists' objects into (RAM) memory.
30
+ #--
31
+ # TODO: fully working query.
32
+ # TODO: arbitrary pk.
33
+ #++
34
+
35
+ class MemoryStore < Store
36
+ extend MemoryUtils; include MemoryUtils
37
+
38
+ class ObjectHash < Hash
39
+ def [](key)
40
+ super(key.to_s)
41
+ end
42
+
43
+ def []=(key, value)
44
+ super(key.to_s, value)
45
+ end
46
+ end
47
+
48
+ # This hash implements an in-memory database.
49
+
50
+ @objects = ObjectHash.new
51
+
52
+ def self.objects
53
+ @objects
54
+ end
55
+
56
+ def self.objects=(val)
57
+ @objects = val
58
+ end
59
+
60
+ # A pseudo-connection to the actual object store.
61
+
62
+ attr_accessor :conn
63
+
64
+ def self.destroy(options)
65
+ FileUtils.rm_rf("#{options[:name]}.db")
66
+ rescue
67
+ Logger.info "Cannot destroy '#{name}'"
68
+ end
69
+
70
+ def initialize(options)
71
+ super
72
+ begin
73
+ @conn = self.class.objects = YAML.load_file("#{@options[:name]}.db")
74
+ rescue
75
+ @conn = self.class.objects = ObjectHash.new
76
+ end
77
+ end
78
+
79
+ def close
80
+ File.open("#{@options[:name]}.db", 'w') { |f| f << @conn.to_yaml }
81
+ end
82
+
83
+ # Enchants a class.
84
+
85
+ def enchant(klass, manager)
86
+ klass.property :oid, Fixnum
87
+
88
+ super
89
+
90
+ @conn[klass] ||= {}
91
+
92
+ eval_og_insert(klass)
93
+ eval_og_update(klass)
94
+ eval_og_read(klass)
95
+ eval_og_delete(klass)
96
+ end
97
+
98
+ # :section: Lifecycle methods.
99
+
100
+ # Loads an object from the store using the primary key.
101
+
102
+ def load(pk, klass)
103
+ @conn[klass][pk]
104
+ end
105
+
106
+ # Update selected properties of an object or class of
107
+ # objects.
108
+
109
+ def update_properties(target, set, options = nil)
110
+ set = set.gsub(/,/, ';')
111
+ if target.is_a?(Class)
112
+ if options
113
+ condition = options[:condition] || options[:where]
114
+ else
115
+ condition = 'true'
116
+ end
117
+ for obj in @conn[target].values
118
+ obj.instance_eval %{
119
+ if #{condition.gsub(/=/, '==')}
120
+ #{set}
121
+ end
122
+ }
123
+ end
124
+ else
125
+ target.instance_eval(set)
126
+ end
127
+ end
128
+ alias_method :pupdate, :update_properties
129
+ alias_method :update_property, :update_properties
130
+
131
+ # Find a collection of objects.
132
+ #
133
+ # === Examples
134
+ #
135
+ # User.find(:condition => 'age > 15', :order => 'score ASC', :offet => 10, :limit =>10)
136
+ # Comment.find(:include => :entry)
137
+ # store.find(:class => User, :where => 'age > 15')
138
+
139
+ def find(options)
140
+ query(options)
141
+ end
142
+
143
+ # Find one object.
144
+
145
+ def find_one(options)
146
+ query(options).first
147
+ end
148
+
149
+ # Reloads an object from the store.
150
+
151
+ def reload(obj, pk)
152
+ # Nop, the memory store is always synchronized.
153
+ end
154
+
155
+ # Count results.
156
+
157
+ def count(options)
158
+ objects = 0
159
+
160
+ if condition = options[:condition] || options[:where]
161
+ condition = "obj." + condition.gsub(/=/, '==')
162
+ else
163
+ condition = true
164
+ end
165
+
166
+ eval %{
167
+ for obj in @conn[options[:class]].values
168
+ objects += 1 if #{condition}
169
+ end
170
+ }
171
+
172
+ return objects
173
+ end
174
+
175
+ # Relate two objects through an intermediate join table.
176
+ # Typically used in joins_many and many_to_many relations.
177
+
178
+ def join(obj1, obj2, table)
179
+ # nop
180
+ end
181
+
182
+ # Query.
183
+
184
+ def query(options)
185
+ objects = []
186
+
187
+ if condition = options[:condition] || options[:where]
188
+ condition = "obj." + condition.gsub(/=/, '==')
189
+ else
190
+ condition = true
191
+ end
192
+
193
+ eval %{
194
+ for obj in @conn[options[:class]].values
195
+ objects << obj if #{condition}
196
+ end
197
+ }
198
+
199
+ if order = options[:order]
200
+ desc = (order =~ /DESC/)
201
+ order = order.gsub(/DESC/, '').gsub(/ASC/, '')
202
+ eval "objects.sort { |x, y| x.#{order} <=> y.#{order} }"
203
+ objects.reverse! if desc
204
+ end
205
+
206
+ return objects
207
+ end
208
+
209
+ # :section: Transaction methods.
210
+
211
+ # Start a new transaction.
212
+
213
+ def start
214
+ end
215
+
216
+ # Commit a transaction.
217
+
218
+ def commit
219
+ end
220
+
221
+ # Rollback a transaction.
222
+
223
+ def rollback
224
+ end
225
+
226
+ private
227
+
228
+ # :section: Lifecycle method compilers.
229
+
230
+ # Compile the og_update method for the class.
231
+
232
+ def eval_og_insert(klass)
233
+ pk = klass.primary_key.first
234
+
235
+ klass.class_eval %{
236
+ def og_insert(store)
237
+ #{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
238
+ @#{pk} = store.conn[#{klass}].size + 1
239
+ store.conn[#{klass}][@#{pk}] = self
240
+ #{Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
241
+ end
242
+ }
243
+ end
244
+
245
+ # Compile the og_update method for the class.
246
+
247
+ def eval_og_update(klass)
248
+ pk = klass.primary_key.first
249
+
250
+ klass.class_eval %{
251
+ def og_update(store)
252
+ #{Aspects.gen_advice_code(:og_update, klass.advices, :pre) if klass.respond_to?(:advices)}
253
+ store.conn[#{klass}][@#{pk}] = self
254
+ #{Aspects.gen_advice_code(:og_update, klass.advices, :post) if klass.respond_to?(:advices)}
255
+ end
256
+ }
257
+ end
258
+
259
+ # Not really useful in this store, kept for compatibility,
260
+ # just to call the aspects.
261
+
262
+ def eval_og_read(klass)
263
+ klass.class_eval %{
264
+ def og_read
265
+ #{Aspects.gen_advice_code(:og_read, klass.advices, :pre) if klass.respond_to?(:advices)}
266
+ #{Aspects.gen_advice_code(:og_read, klass.advices, :post) if klass.respond_to?(:advices)}
267
+ end
268
+ }
269
+ end
270
+
271
+ def eval_og_delete(klass)
272
+ klass.module_eval %{
273
+ def og_delete(store, pk, cascade = true)
274
+ #{Aspects.gen_advice_code(:og_delete, klass.advices, :pre) if klass.respond_to?(:advices)}
275
+ pk ||= @#{klass.primary_key.first}
276
+ transaction do |tx|
277
+ tx.conn[#{klass}].delete(pk)
278
+ if cascade and #{klass}.__meta[:descendants]
279
+ #{klass}.__meta[:descendants].each do |dclass, foreign_key|
280
+ eval "tx.conn[dclass].delete_if { |dobj| dobj.\#{foreign_key} == \#{pk} }"
281
+ end
282
+ end
283
+ end
284
+ #{Aspects.gen_advice_code(:og_delete, klass.advices, :post) if klass.respond_to?(:advices)}
285
+ end
286
+ }
287
+ end
288
+
289
+ end
290
+
291
+ end