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
@@ -291,3 +291,5 @@ private
291
291
  end
292
292
 
293
293
  end
294
+
295
+ # * George Moschovitis <gm@navel.gr>
@@ -129,10 +129,10 @@ private
129
129
 
130
130
  sql = "CREATE TABLE #{klass::OGTABLE} (#{fields.join(', ')}"
131
131
 
132
- # Create table constrains.
132
+ # Create table constraints.
133
133
 
134
- if klass.__meta and constrains = klass.__meta[:sql_constrain]
135
- sql << ", #{constrains.join(', ')}"
134
+ if klass.__meta and constraints = klass.__meta[:sql_constraint]
135
+ sql << ", #{constraints.join(', ')}"
136
136
  end
137
137
 
138
138
  sql << ");"
@@ -167,10 +167,10 @@ private
167
167
  # Create join tables if needed. Join tables are used in
168
168
  # 'many_to_many' relations.
169
169
 
170
- if klass.__meta and join_tables = klass.__meta[:join_tables]
171
- for join_table in join_tables
170
+ if klass.__meta and join_tables = klass.__meta[:join_tables]
171
+ for join_table in join_tables
172
172
  begin
173
- conn.query(create_join_table_sql(join_table))
173
+ conn.query(create_join_table_sql(join_table))
174
174
  rescue => ex
175
175
  # gmosx: any idea how to better test this?
176
176
  if ex.errno == 1050 # table already exists.
@@ -1,125 +1,194 @@
1
1
  begin
2
- require 'lib/vendor/kirbybase'
2
+ require 'vendor/kirbybase'
3
3
  rescue Object => ex
4
4
  Logger.error 'KirbyBase is not installed!'
5
5
  Logger.error ex
6
6
  end
7
7
 
8
+ require 'fileutils'
9
+
8
10
  require 'og/store/sql'
9
11
 
10
12
  module Og
11
13
 
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.
14
+ # A Store that persists objects into a KirbyBase database.
15
+ # To read documentation about the methods, consult the documentation
16
+ # for SqlStore and Store.
16
17
 
17
18
  class KirbyStore < SqlStore
18
19
 
19
- def self.db_dir(options)
20
- "#{options[:name]}_db"
20
+ # Override if needed.
21
+
22
+ def self.base_dir(options)
23
+ options[:base_dir] || './kirbydb'
21
24
  end
22
25
 
23
26
  def self.destroy(options)
24
27
  begin
25
- FileUtils.rm_rf(db_dir(options))
28
+ FileUtils.rm_rf(base_dir(options))
26
29
  super
27
30
  rescue Object
28
- Logger.info "Cannot drop '#{options[:name]}'!"
31
+ Logger.info 'Cannot drop database!'
29
32
  end
30
33
  end
31
34
 
32
35
  def initialize(options)
33
36
  super
37
+ mode = options[:mode] || :local
34
38
 
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
+ if mode == :client
40
+ # Use a client/server configuration.
41
+ @conn = KirbyBase.new(:client, options[:address], options[:port])
39
42
  else
40
- # TODO
43
+ # Use an in-process configuration.
44
+ base_dir = self.class.base_dir(options)
45
+ FileUtils.mkdir_p(base_dir) unless File.exist?(base_dir)
46
+ @conn = KirbyBase.new(
47
+ :local,
48
+ nil, nil,
49
+ base_dir,
50
+ options[:ext] || '.tbl'
51
+ )
41
52
  end
42
53
  end
43
54
 
44
55
  def close
56
+ # Nothing to do.
45
57
  super
46
58
  end
47
59
 
48
60
  def enchant(klass, manager)
49
- klass.property :oid, Fixnum, :sql => 'integer PRIMARY KEY'
61
+ klass.send :attr_accessor, :recno
62
+ klass.send :alias_method, :oid, :recno
63
+ klass.send :alias_method, :oid=, :recno=
64
+
65
+ symbols = klass.properties.keys
66
+
67
+ klass.module_eval %{
68
+ def self.kb_create(recno, #{symbols.join(', ')})
69
+ obj = self.allocate
70
+ obj.recno = recno
71
+ #{ symbols.map { |s| "obj.#{s} = #{s}; "} }
72
+ return obj
73
+ end
74
+ }
75
+
50
76
  super
51
77
  end
52
78
 
53
- def query(sql)
54
- Logger.debug sql if $DBG
55
- return @conn.query(sql)
56
- rescue => ex
57
- handle_sql_exception(ex, sql)
79
+ def get_table(klass)
80
+ @conn.get_table(klass.table.to_sym)
81
+ end
82
+
83
+ # :section: Lifecycle methods.
84
+
85
+ def load(pk, klass)
86
+ get_table(klass)[pk.to_i]
58
87
  end
88
+ alias_method :exist?, :load
59
89
 
60
- def exec(sql)
61
- Logger.debug sql if $DBG
62
- @conn.query(sql).close
63
- rescue => ex
64
- handle_sql_exception(ex, sql)
90
+ def reload(obj, pk)
91
+ raise 'Cannot reload unmanaged object' unless obj.saved?
92
+ new_obj = load(pk, obj.class)
93
+ obj.clone(new_obj)
94
+ end
95
+
96
+ def find(options)
97
+ query(options)
98
+ end
99
+
100
+ def find_one(options)
101
+ query(options).first
102
+ end
103
+
104
+ #--
105
+ # FIXME: optimize me!
106
+ #++
107
+
108
+ def count(options)
109
+ find(options).size
65
110
  end
66
111
 
67
- def start
112
+ def query(options)
113
+ Logger.debug "Querying with #{options.inspect}." if $DBG
114
+
115
+ klass = options[:class]
116
+ table = get_table(klass)
117
+
118
+ objects = []
119
+
120
+ if condition = options[:condition] || options[:where]
121
+ condition.gsub!(/=/, '==')
122
+ condition.gsub!(/LIKE '(.*?)'/, '=~ /\1/')
123
+ condition.gsub!(/\%/, '')
124
+ condition.gsub!(/(\w*?)\s?=(.)/, 'o.\1 =\2')
125
+ objects = eval("table.select { |o| #{condition} }")
126
+ else
127
+ objects = table.select
128
+ end
129
+
130
+ if order = options[:order]
131
+ desc = (order =~ /DESC/)
132
+ order = order.gsub(/DESC/, '').gsub(/ASC/, '')
133
+ eval "objects.sort { |x, y| x.#{order} <=> y.#{order} }"
134
+ objects.reverse! if desc
135
+ end
136
+
137
+ return objects
138
+ end
139
+
140
+ def start
68
141
  # nop
69
142
  end
70
143
 
144
+ # Commit a transaction.
145
+
71
146
  def commit
72
- # nop
147
+ # nop, not supported?
73
148
  end
74
149
 
150
+ # Rollback a transaction.
151
+
75
152
  def rollback
76
- # nop
153
+ # nop, not supported?
154
+ end
155
+
156
+ def sql_update(sql)
157
+ # nop, not supported.
77
158
  end
78
159
 
79
160
  private
80
161
 
81
162
  def create_table(klass)
82
163
  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
164
+ begin
165
+ table = @conn.create_table(klass.table.to_sym, *fields) do |t|
166
+ t.record_class = klass
167
+ end
168
+ rescue Object => ex
169
+ # gmosx: any idea how to better test this?
170
+ if ex.to_s =~ /already exists/i
171
+ Logger.debug "Table for '#{klass}' already exists!"
172
+ return
173
+ else
174
+ raise
106
175
  end
107
176
  end
108
177
  end
109
178
 
110
179
  def drop_table(klass)
111
- @conn.drop_table(klass.table) if @conn.table_exists?(klass.table)
180
+ @conn.drop_table(klass.table) if @conn.table_exists?(klass.table)
112
181
  end
113
182
 
114
183
  def fields_for_class(klass)
115
184
  fields = []
116
185
 
117
- klass.properties.each do |p|
118
- klass.index(p.symbol) if p.meta[:index]
186
+ klass.properties.values.each do |p|
187
+ klass.index(p.symbol) if p.index
119
188
 
120
189
  fields << p.symbol
121
190
 
122
- type = p.klass.name.intern
191
+ type = p.klass.name.to_sym
123
192
  type = :Integer if type == :Fixnum
124
193
 
125
194
  fields << type
@@ -128,153 +197,67 @@ private
128
197
  return fields
129
198
  end
130
199
 
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
200
+ def eval_og_insert(klass)
201
+ pk = klass.primary_key.symbol
141
202
 
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)\}'" : "''"|
203
+ if klass.schema_inheritance?
204
+ props << Property.new(:symbol => :ogtype, :klass => String)
205
+ values << ", '#{klass}'"
166
206
  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
207
 
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 %{
208
+ klass.class_eval %{
202
209
  def og_insert(store)
203
210
  #{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
204
- store.conn.get_table('#{klass.table}').insert(#{data})
211
+ Logger.debug "Inserting \#{self}." if $DBG
212
+ @#{pk} = store.get_table(#{klass}).insert(self)
205
213
  #{Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
206
214
  end
207
215
  }
208
216
  end
209
-
217
+
210
218
  # Compile the og_update method for the class.
211
219
 
212
220
  def eval_og_update(klass)
213
221
  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}\}"
222
+ updates = klass.properties.keys.collect { |p| ":#{p} => @#{p}" }
221
223
 
222
224
  klass.module_eval %{
223
- def og_update(store)
225
+ def og_update(store, options = nil)
224
226
  #{Aspects.gen_advice_code(:og_update, klass.advices, :pre) if klass.respond_to?(:advices)}
225
- store.exec "#{sql}"
227
+ store.get_table(#{klass}).update { |r| r.recno == #{pk} }.set(#{updates.join(', ')})
226
228
  #{Aspects.gen_advice_code(:og_update, klass.advices, :post) if klass.respond_to?(:advices)}
227
229
  end
228
230
  }
229
231
  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
232
 
236
233
  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
234
  klass.module_eval %{
250
235
  def og_read(res, row = 0, offset = 0)
251
236
  #{Aspects.gen_advice_code(:og_read, klass.advices, :pre) if klass.respond_to?(:advices)}
252
- #{code}
253
237
  #{Aspects.gen_advice_code(:og_read, klass.advices, :post) if klass.respond_to?(:advices)}
254
238
  end
255
239
  }
256
240
  end
257
241
 
258
- #--
259
- # FIXME: is pk needed as parameter?
260
- #++
261
-
262
242
  def eval_og_delete(klass)
263
243
  klass.module_eval %{
264
244
  def og_delete(store, pk, cascade = true)
245
+ pk ||= self.pk
246
+ pk = pk.to_i
265
247
  #{Aspects.gen_advice_code(:og_delete, klass.advices, :pre) if klass.respond_to?(:advices)}
266
- pk ||= @#{klass.pk_symbol}
248
+ table = store.get_table(self.class)
267
249
  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}"
250
+ table.delete { |r| r.recno == pk }
251
+ if cascade and #{klass}.ann.this[:descendants]
252
+ #{klass}.ann.this.descendants.each do |dclass, foreign_key|
253
+ dtable = store.get_table(dclass)
254
+ dtable.delete { |r| foreign_key == pk }
272
255
  end
273
256
  end
274
257
  end
275
258
  #{Aspects.gen_advice_code(:og_delete, klass.advices, :post) if klass.respond_to?(:advices)}
276
259
  end
277
- }
260
+ }
278
261
  end
279
262
 
280
263
  end