og 0.23.0 → 0.24.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 (55) hide show
  1. data/ProjectInfo +58 -0
  2. data/README +5 -4
  3. data/Rakefile +2 -2
  4. data/doc/AUTHORS +10 -7
  5. data/doc/RELEASES +108 -0
  6. data/lib/og.rb +1 -3
  7. data/lib/og/collection.rb +4 -4
  8. data/lib/og/entity.rb +96 -27
  9. data/lib/og/evolution.rb +78 -0
  10. data/lib/og/manager.rb +29 -32
  11. data/lib/og/mixin/hierarchical.rb +1 -1
  12. data/lib/og/mixin/optimistic_locking.rb +5 -8
  13. data/lib/og/mixin/orderable.rb +15 -2
  14. data/lib/og/mixin/schema_inheritance_base.rb +12 -0
  15. data/lib/og/mixin/taggable.rb +29 -25
  16. data/lib/og/mixin/timestamped.rb +4 -2
  17. data/lib/og/mixin/tree.rb +0 -1
  18. data/lib/og/relation.rb +161 -116
  19. data/lib/og/relation/all.rb +6 -0
  20. data/lib/og/relation/belongs_to.rb +4 -1
  21. data/lib/og/relation/has_many.rb +6 -5
  22. data/lib/og/relation/joins_many.rb +13 -12
  23. data/lib/og/relation/refers_to.rb +3 -3
  24. data/lib/og/store.rb +9 -9
  25. data/lib/og/store/{filesys.rb → alpha/filesys.rb} +0 -0
  26. data/lib/og/store/alpha/kirby.rb +284 -0
  27. data/lib/og/store/{memory.rb → alpha/memory.rb} +2 -0
  28. data/lib/og/store/{sqlserver.rb → alpha/sqlserver.rb} +6 -6
  29. data/lib/og/store/kirby.rb +145 -162
  30. data/lib/og/store/mysql.rb +58 -27
  31. data/lib/og/store/psql.rb +15 -13
  32. data/lib/og/store/sql.rb +136 -135
  33. data/lib/og/store/sqlite.rb +13 -12
  34. data/lib/og/validation.rb +2 -2
  35. data/lib/vendor/kbserver.rb +20 -0
  36. data/lib/vendor/kirbybase.rb +2790 -1601
  37. data/test/og/CONFIG.rb +79 -0
  38. data/test/og/mixin/tc_hierarchical.rb +1 -1
  39. data/test/og/mixin/tc_optimistic_locking.rb +1 -3
  40. data/test/og/mixin/tc_orderable.rb +42 -1
  41. data/test/og/mixin/tc_taggable.rb +1 -1
  42. data/test/og/mixin/tc_timestamped.rb +1 -1
  43. data/test/og/store/tc_filesys.rb +1 -2
  44. data/test/og/tc_delete_all.rb +45 -0
  45. data/test/og/tc_inheritance.rb +10 -38
  46. data/test/og/tc_join.rb +2 -11
  47. data/test/og/tc_multiple.rb +3 -16
  48. data/test/og/tc_override.rb +3 -3
  49. data/test/og/tc_polymorphic.rb +3 -13
  50. data/test/og/tc_relation.rb +8 -6
  51. data/test/og/tc_reverse.rb +2 -11
  52. data/test/og/tc_select.rb +2 -15
  53. data/test/og/tc_store.rb +4 -63
  54. data/test/og/tc_types.rb +1 -2
  55. metadata +80 -77
@@ -0,0 +1,6 @@
1
+ require 'og/relation/belongs_to'
2
+ require 'og/relation/has_many'
3
+ require 'og/relation/has_one'
4
+ require 'og/relation/joins_many'
5
+ require 'og/relation/many_to_many'
6
+ require 'og/relation/refers_to'
@@ -6,7 +6,10 @@ class BelongsTo < RefersTo
6
6
 
7
7
  def enchant
8
8
  super
9
- target_class.meta :descendants, [owner_class, foreign_key]
9
+ unless target_class.ann.this[:descendants]
10
+ target_class.ann(:this, :descendants => [])
11
+ end
12
+ target_class.ann.this.descendants! << [owner_class, foreign_key]
10
13
  end
11
14
 
12
15
  end
@@ -19,7 +19,7 @@ end
19
19
 
20
20
  class HasMany < Relation
21
21
  def resolve_polymorphic
22
- if target_class.relations
22
+ unless target_class.relations.empty?
23
23
  unless target_class.relations.find { |r| r.is_a?(BelongsTo) and r.target_class == owner_class }
24
24
  target_class.belongs_to(owner_class)
25
25
  end
@@ -29,7 +29,7 @@ class HasMany < Relation
29
29
  def enchant
30
30
  self[:owner_singular_name] = owner_class.to_s.demodulize.underscore.downcase
31
31
  self[:target_singular_name] = target_plural_name.to_s.singular
32
- self[:foreign_key] = "#{foreign_name || owner_singular_name}_#{owner_pk}"
32
+ self[:foreign_key] = "#{foreign_name || owner_singular_name}_#{owner_class.primary_key}"
33
33
  # gmosx: DON'T set self[:foreign_field]
34
34
  foreign_field = self[:foreign_field] || self[:foreign_key]
35
35
 
@@ -55,17 +55,18 @@ class HasMany < Relation
55
55
 
56
56
  def add_#{target_singular_name}(obj, options = nil)
57
57
  return unless obj
58
- obj.#{foreign_key} = @#{owner_pk}
58
+ obj.#{foreign_key} = @#{owner_class.primary_key}
59
59
  obj.save
60
60
  end
61
61
 
62
62
  def remove_#{target_singular_name}(obj)
63
63
  obj.#{foreign_key} = nil
64
+ obj.save
64
65
  end
65
66
 
66
67
  def find_#{target_plural_name}(options = {})
67
68
  find_options = {
68
- :condition => "#{foreign_field} = \#{og_quote(@#{owner_pk})}"
69
+ :condition => "#{foreign_field} = \#{og_quote(#{owner_class.primary_key})}"
69
70
  }
70
71
  if options
71
72
  if condition = options.delete(:condition)
@@ -77,7 +78,7 @@ class HasMany < Relation
77
78
  end
78
79
 
79
80
  def count_#{target_plural_name}
80
- #{target_class}.count(:condition => "#{foreign_field} = \#{og_quote(@#{owner_pk})}")
81
+ #{target_class}.count(:condition => "#{foreign_field} = \#{og_quote(#{owner_class.primary_key})}")
81
82
  end
82
83
  }
83
84
  end
@@ -28,25 +28,26 @@ class JoinsMany < Relation
28
28
  # handle schema_inheritance
29
29
 
30
30
  join_class = owner_class
31
- if sclass = owner_class.metadata.superclass
32
- join_class = sclass.first
31
+ if sclass = owner_class.ann.this[:superclass]
32
+ join_class = sclass
33
33
  end
34
34
  join_table_info = store.join_table_info(join_class, target_class)
35
35
 
36
36
  if through = self[:through]
37
37
  # A custom class is used for the join. Use the class
38
38
  # table and don't create a new table.
39
- through = resolve_symbol(through, owner_class) if through.is_a?(Symbol)
39
+ through = Relation.symbol_to_class(through, owner_class) if through.is_a?(Symbol)
40
40
  join_table = self[:join_table] = through.table
41
41
  else
42
42
  # Calculate the name of the join table
43
43
  join_table = self[:join_table] = join_table_info[:table]
44
- # Create a join table.
45
- owner_class.meta :join_tables, join_table_info
44
+ # Create a join table.
45
+ owner_class.ann :this, :join_tables => []
46
+ owner_class.ann.this.join_tables! << join_table_info
46
47
  end
47
48
 
48
- owner_key = join_table_info[:owner_key]
49
- target_key = join_table_info[:target_key]
49
+ owner_key = join_table_info[:owner_key]
50
+ target_key = join_table_info[:target_key]
50
51
 
51
52
  owner_class.module_eval %{
52
53
  attr_accessor :#{target_plural_name}
@@ -82,8 +83,8 @@ class JoinsMany < Relation
82
83
  def find_#{target_plural_name}(options = {})
83
84
  find_options = {
84
85
  :join_table => "#{join_table}",
85
- :join_condition => "#{join_table}.#{target_key}=#{store.table(target_class)}.oid",
86
- :condition => "#{join_table}.#{owner_key}=\#\{@#{owner_pk}\}"
86
+ :join_condition => "#{join_table}.#{target_key}=\#{#{target_class}::OGTABLE}.oid",
87
+ :condition => "#{join_table}.#{owner_key}=\#\{#{owner_class.primary_key}\}"
87
88
  }
88
89
  if options
89
90
  if condition = options.delete(:condition)
@@ -101,8 +102,8 @@ class JoinsMany < Relation
101
102
  def count_#{target_plural_name}
102
103
  find_options = {
103
104
  :join_table => "#{join_table}",
104
- :join_condition => "#{join_table}.#{target_key}=#{store.table(target_class)}.oid",
105
- :condition => "#{join_table}.#{owner_key}=\#\{@#{owner_pk}\}"
105
+ :join_condition => "#{join_table}.#{target_key}=\#{#{target_class}::OGTABLE}.oid",
106
+ :condition => "#{join_table}.#{owner_key}=\#\{#{owner_class.primary_key}\}"
106
107
  }
107
108
  #{target_class}.find(find_options).size
108
109
  end
@@ -111,7 +112,7 @@ class JoinsMany < Relation
111
112
  if through
112
113
  owner_class.module_eval %{
113
114
  def #{target_singular_name}_join_data(t)
114
- #{through}.find_one(:condition => "#{owner_key}=\#{@#{owner_pk}} and #{target_key}=\#{t.pk}")
115
+ #{through}.find_one(:condition => "#{owner_key}=\#{#{owner_class.primary_key}} and #{target_key}=\#{t.pk}")
115
116
  end
116
117
  }
117
118
  end
@@ -5,7 +5,7 @@ module Og
5
5
  class RefersTo < Relation
6
6
 
7
7
  def enchant
8
- self[:foreign_key] = "#{foreign_name || target_singular_name}_#{target_pk}"
8
+ self[:foreign_key] = "#{foreign_name || target_singular_name}_#{target_class.primary_key}"
9
9
 
10
10
  if self[:field]
11
11
  field = ", :field => :#{self[:field]}"
@@ -13,7 +13,7 @@ class RefersTo < Relation
13
13
 
14
14
  owner_class.module_eval %{
15
15
  attr_accessor :#{target_singular_name}
16
- prop_accessor :#{foreign_key}, #{target_pkclass}#{field}, :relation => true
16
+ prop_accessor :#{foreign_key}, #{target_class.primary_key.klass}#{field}, :relation => true
17
17
 
18
18
  def #{target_singular_name}(reload = false)
19
19
  return nil if @#{foreign_key}.nil?
@@ -26,7 +26,7 @@ class RefersTo < Relation
26
26
  end
27
27
 
28
28
  def #{target_singular_name}=(obj)
29
- @#{foreign_key} = obj.#{target_pk} if obj
29
+ @#{foreign_key} = obj.#{target_class.primary_key} if obj
30
30
  end
31
31
  }
32
32
  end
@@ -55,15 +55,15 @@ class Store
55
55
  meta :index, arg
56
56
  end
57
57
 
58
- pk = klass.primary_key.first
58
+ pk = klass.primary_key.symbol
59
59
 
60
60
  klass.module_eval %{
61
61
  def saved?
62
- return @#{klass.primary_key.first}
62
+ return #{klass.primary_key.symbol}
63
63
  end
64
64
 
65
65
  def unsaved?
66
- return !@#{klass.primary_key.first}
66
+ return !#{klass.primary_key.symbol}
67
67
  end
68
68
 
69
69
  # Evaluate an alias for the primary key.
@@ -72,7 +72,7 @@ class Store
72
72
  alias_method :pk=, :#{pk}=
73
73
 
74
74
  def self.pk_symbol
75
- :#{klass.primary_key.first}
75
+ :#{klass.primary_key.symbol}
76
76
  end
77
77
  }
78
78
 
@@ -80,16 +80,16 @@ class Store
80
80
 
81
81
  code = ''
82
82
 
83
- for p in klass.properties
84
- # gmosx: :uniq does NOT set a unique constrain in the
83
+ for p in klass.properties.values
84
+ # gmosx: :uniq does NOT set a unique constraint in the
85
85
  # database.
86
- finder = p.meta[:uniq] || p.meta[:unique] ? 'find_one' : 'find'
86
+ finder = p.uniq || p.unique ? 'find_one' : 'find'
87
87
 
88
88
  code << %{
89
- def self.find_by_#{p.symbol}(val, operator = '=', options = {})
89
+ def self.find_by_#{p}(val, operator = '=', options = {})
90
90
  options.update(
91
91
  :class => #{klass},
92
- :condition => "#{p.meta[:field] || p.symbol}\#{operator}\#{ogmanager.store.quote(val)}"
92
+ :condition => "#{p.field || p}\#{operator}\#{ogmanager.store.quote(val)}"
93
93
  )
94
94
  ogmanager.store.#{finder}(options)
95
95
  end;
@@ -0,0 +1,284 @@
1
+ begin
2
+ require 'lib/vendor/kirbybase'
3
+ rescue Object => ex
4
+ Logger.error 'KirbyBase is not installed!'
5
+ Logger.error ex
6
+ end
7
+
8
+ require 'og/store/sql'
9
+
10
+ module Og
11
+
12
+ # A Store that persists objects into an KirbyBase database.
13
+ # KirbyBase is a pure-ruby database implementation.
14
+ # To read documentation about the methods, consult the
15
+ # documentation for SqlStore and Store.
16
+
17
+ class KirbyStore < SqlStore
18
+
19
+ def self.db_dir(options)
20
+ "#{options[:name]}_db"
21
+ end
22
+
23
+ def self.destroy(options)
24
+ begin
25
+ FileUtils.rm_rf(db_dir(options))
26
+ super
27
+ rescue Object
28
+ Logger.info "Cannot drop '#{options[:name]}'!"
29
+ end
30
+ end
31
+
32
+ def initialize(options)
33
+ super
34
+
35
+ if options[:embedded]
36
+ name = self.class.db_dir(options)
37
+ FileUtils.mkdir_p(name)
38
+ @conn = KirbyBase.new(:local, nil, nil, name)
39
+ else
40
+ # TODO
41
+ end
42
+ end
43
+
44
+ def close
45
+ super
46
+ end
47
+
48
+ def enchant(klass, manager)
49
+ klass.property :oid, Fixnum, :sql => 'integer PRIMARY KEY'
50
+ super
51
+ end
52
+
53
+ def query(sql)
54
+ Logger.debug sql if $DBG
55
+ return @conn.query(sql)
56
+ rescue => ex
57
+ handle_sql_exception(ex, sql)
58
+ end
59
+
60
+ def exec(sql)
61
+ Logger.debug sql if $DBG
62
+ @conn.query(sql).close
63
+ rescue => ex
64
+ handle_sql_exception(ex, sql)
65
+ end
66
+
67
+ def start
68
+ # nop
69
+ end
70
+
71
+ def commit
72
+ # nop
73
+ end
74
+
75
+ def rollback
76
+ # nop
77
+ end
78
+
79
+ private
80
+
81
+ def create_table(klass)
82
+ fields = fields_for_class(klass)
83
+
84
+ table = @conn.create_table(klass::OGTABLE, *fields) { |obj| obj.encrypt = false }
85
+
86
+ # Create join tables if needed. Join tables are used in
87
+ # 'many_to_many' relations.
88
+
89
+ if klass.__meta and join_tables = klass.__meta[:join_tables]
90
+ for join_table in join_tables
91
+ begin
92
+ @conn.create_table(join_table[:table],
93
+ join_table[:first_key], :Integer,
94
+ join_table[:second_key], :Integer)
95
+ # KirbyBase doesn't support indices.
96
+ rescue RuntimeError => error
97
+ # Unfortunately, KirbyBase just throws RuntimeErrors
98
+ # with no extra information, so we just have to look
99
+ # for the error message it uses.
100
+ if error.message =~ /table #{join_table[:table]} already exists/i
101
+ Logger.debug 'Join table already exists' if $DBG
102
+ else
103
+ raise
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ def drop_table(klass)
111
+ @conn.drop_table(klass.table) if @conn.table_exists?(klass.table)
112
+ end
113
+
114
+ def fields_for_class(klass)
115
+ fields = []
116
+
117
+ klass.properties.each do |p|
118
+ klass.index(p.symbol) if p.meta[:index]
119
+
120
+ fields << p.symbol
121
+
122
+ type = p.klass.name.intern
123
+ type = :Integer if type == :Fixnum
124
+
125
+ fields << type
126
+ end
127
+
128
+ return fields
129
+ end
130
+
131
+ def create_field_map(klass)
132
+ map = {}
133
+ fields = @conn.get_table(klass.table).field_names
134
+
135
+ fields.size.times do |i|
136
+ map[fields[i]] = i
137
+ end
138
+
139
+ return map
140
+ end
141
+
142
+ # Return an sql string evaluator for the property.
143
+ # No need to optimize this, used only to precalculate code.
144
+ # YAML is used to store general Ruby objects to be more
145
+ # portable.
146
+ #--
147
+ # FIXME: add extra handling for float.
148
+ #++
149
+
150
+ def write_prop(p)
151
+ if p.klass.ancestors.include?(Integer)
152
+ return "@#{p.symbol} || nil"
153
+ elsif p.klass.ancestors.include?(Float)
154
+ return "@#{p.symbol} || nil"
155
+ elsif p.klass.ancestors.include?(String)
156
+ return %|@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol})\}'" : nil|
157
+ elsif p.klass.ancestors.include?(Time)
158
+ return %|@#{p.symbol} ? "'#\{#{self.class}.timestamp(@#{p.symbol})\}'" : nil|
159
+ elsif p.klass.ancestors.include?(Date)
160
+ return %|@#{p.symbol} ? "'#\{#{self.class}.date(@#{p.symbol})\}'" : nil|
161
+ elsif p.klass.ancestors.include?(TrueClass)
162
+ return "@#{p.symbol} ? \"'t'\" : nil"
163
+ else
164
+ # gmosx: keep the '' for nil symbols.
165
+ return %|@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol}.to_yaml)\}'" : "''"|
166
+ end
167
+ end
168
+
169
+ # Return an evaluator for reading the property.
170
+ # No need to optimize this, used only to precalculate code.
171
+
172
+ def read_prop(p, col)
173
+ if p.klass.ancestors.include?(Integer)
174
+ return "#{self.class}.parse_int(res[#{col} + offset])"
175
+ elsif p.klass.ancestors.include?(Float)
176
+ return "#{self.class}.parse_float(res[#{col} + offset])"
177
+ elsif p.klass.ancestors.include?(String)
178
+ return "res[#{col} + offset]"
179
+ elsif p.klass.ancestors.include?(Time)
180
+ return "#{self.class}.parse_timestamp(res[#{col} + offset])"
181
+ elsif p.klass.ancestors.include?(Date)
182
+ return "#{self.class}.parse_date(res[#{col} + offset])"
183
+ elsif p.klass.ancestors.include?(TrueClass)
184
+ return "('0' != res[#{col} + offset])"
185
+ else
186
+ return "YAML::load(res[#{col} + offset])"
187
+ end
188
+ end
189
+
190
+ # :section: Lifecycle method compilers.
191
+
192
+ # Compile the og_update method for the class.
193
+
194
+ def eval_og_insert(klass)
195
+ pk = klass.pk_symbol
196
+ props = klass.properties
197
+
198
+ data = props.collect {|p| ":#{p.symbol} => #{write_prop(p)}"}.join(', ')
199
+ # data.gsub!(/#|\{|\}/, '')
200
+
201
+ klass.module_eval %{
202
+ def og_insert(store)
203
+ #{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
204
+ store.conn.get_table('#{klass.table}').insert(#{data})
205
+ #{Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
206
+ end
207
+ }
208
+ end
209
+
210
+ # Compile the og_update method for the class.
211
+
212
+ def eval_og_update(klass)
213
+ pk = klass.pk_symbol
214
+ props = klass.properties.reject { |p| pk == p.symbol }
215
+
216
+ updates = props.collect { |p|
217
+ "#{p.symbol}=#{write_prop(p)}"
218
+ }
219
+
220
+ sql = "UPDATE #{klass::OGTABLE} SET #{updates.join(', ')} WHERE #{pk}=#\{@#{pk}\}"
221
+
222
+ klass.module_eval %{
223
+ def og_update(store)
224
+ #{Aspects.gen_advice_code(:og_update, klass.advices, :pre) if klass.respond_to?(:advices)}
225
+ store.exec "#{sql}"
226
+ #{Aspects.gen_advice_code(:og_update, klass.advices, :post) if klass.respond_to?(:advices)}
227
+ end
228
+ }
229
+ end
230
+
231
+ # Compile the og_read method for the class. This method is
232
+ # used to read (deserialize) the given class from the store.
233
+ # In order to allow for changing field/attribute orders a
234
+ # field mapping hash is used.
235
+
236
+ def eval_og_read(klass)
237
+ code = []
238
+ props = klass.properties
239
+ field_map = create_field_map(klass)
240
+
241
+ props.each do |p|
242
+ if col = field_map[p.symbol]
243
+ code << "@#{p.symbol} = #{read_prop(p, col)}"
244
+ end
245
+ end
246
+
247
+ code = code.join('; ')
248
+
249
+ klass.module_eval %{
250
+ def og_read(res, row = 0, offset = 0)
251
+ #{Aspects.gen_advice_code(:og_read, klass.advices, :pre) if klass.respond_to?(:advices)}
252
+ #{code}
253
+ #{Aspects.gen_advice_code(:og_read, klass.advices, :post) if klass.respond_to?(:advices)}
254
+ end
255
+ }
256
+ end
257
+
258
+ #--
259
+ # FIXME: is pk needed as parameter?
260
+ #++
261
+
262
+ def eval_og_delete(klass)
263
+ klass.module_eval %{
264
+ def og_delete(store, pk, cascade = true)
265
+ #{Aspects.gen_advice_code(:og_delete, klass.advices, :pre) if klass.respond_to?(:advices)}
266
+ pk ||= @#{klass.pk_symbol}
267
+ transaction do |tx|
268
+ tx.exec "DELETE FROM #{klass.table} WHERE #{klass.pk_symbol}=\#{pk}"
269
+ if cascade and #{klass}.__meta[:descendants]
270
+ #{klass}.__meta[:descendants].each do |dclass, foreign_key|
271
+ tx.exec "DELETE FROM \#{dclass::OGTABLE} WHERE \#{foreign_key}=\#{pk}"
272
+ end
273
+ end
274
+ end
275
+ #{Aspects.gen_advice_code(:og_delete, klass.advices, :post) if klass.respond_to?(:advices)}
276
+ end
277
+ }
278
+ end
279
+
280
+ end
281
+
282
+ end
283
+
284
+ # * George Moschovitis <gm@navel.gr>