og 0.23.0 → 0.24.0

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