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.
- data/ProjectInfo +58 -0
- data/README +5 -4
- data/Rakefile +2 -2
- data/doc/AUTHORS +10 -7
- data/doc/RELEASES +108 -0
- data/lib/og.rb +1 -3
- data/lib/og/collection.rb +4 -4
- data/lib/og/entity.rb +96 -27
- data/lib/og/evolution.rb +78 -0
- data/lib/og/manager.rb +29 -32
- data/lib/og/mixin/hierarchical.rb +1 -1
- data/lib/og/mixin/optimistic_locking.rb +5 -8
- data/lib/og/mixin/orderable.rb +15 -2
- data/lib/og/mixin/schema_inheritance_base.rb +12 -0
- data/lib/og/mixin/taggable.rb +29 -25
- data/lib/og/mixin/timestamped.rb +4 -2
- data/lib/og/mixin/tree.rb +0 -1
- data/lib/og/relation.rb +161 -116
- data/lib/og/relation/all.rb +6 -0
- data/lib/og/relation/belongs_to.rb +4 -1
- data/lib/og/relation/has_many.rb +6 -5
- data/lib/og/relation/joins_many.rb +13 -12
- data/lib/og/relation/refers_to.rb +3 -3
- data/lib/og/store.rb +9 -9
- data/lib/og/store/{filesys.rb → alpha/filesys.rb} +0 -0
- data/lib/og/store/alpha/kirby.rb +284 -0
- data/lib/og/store/{memory.rb → alpha/memory.rb} +2 -0
- data/lib/og/store/{sqlserver.rb → alpha/sqlserver.rb} +6 -6
- data/lib/og/store/kirby.rb +145 -162
- data/lib/og/store/mysql.rb +58 -27
- data/lib/og/store/psql.rb +15 -13
- data/lib/og/store/sql.rb +136 -135
- data/lib/og/store/sqlite.rb +13 -12
- data/lib/og/validation.rb +2 -2
- data/lib/vendor/kbserver.rb +20 -0
- data/lib/vendor/kirbybase.rb +2790 -1601
- data/test/og/CONFIG.rb +79 -0
- data/test/og/mixin/tc_hierarchical.rb +1 -1
- data/test/og/mixin/tc_optimistic_locking.rb +1 -3
- data/test/og/mixin/tc_orderable.rb +42 -1
- data/test/og/mixin/tc_taggable.rb +1 -1
- data/test/og/mixin/tc_timestamped.rb +1 -1
- data/test/og/store/tc_filesys.rb +1 -2
- data/test/og/tc_delete_all.rb +45 -0
- data/test/og/tc_inheritance.rb +10 -38
- data/test/og/tc_join.rb +2 -11
- data/test/og/tc_multiple.rb +3 -16
- data/test/og/tc_override.rb +3 -3
- data/test/og/tc_polymorphic.rb +3 -13
- data/test/og/tc_relation.rb +8 -6
- data/test/og/tc_reverse.rb +2 -11
- data/test/og/tc_select.rb +2 -15
- data/test/og/tc_store.rb +4 -63
- data/test/og/tc_types.rb +1 -2
- metadata +80 -77
@@ -129,10 +129,10 @@ private
|
|
129
129
|
|
130
130
|
sql = "CREATE TABLE #{klass::OGTABLE} (#{fields.join(', ')}"
|
131
131
|
|
132
|
-
# Create table
|
132
|
+
# Create table constraints.
|
133
133
|
|
134
|
-
if klass.__meta and
|
135
|
-
sql << ", #{
|
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.
|
data/lib/og/store/kirby.rb
CHANGED
@@ -1,125 +1,194 @@
|
|
1
1
|
begin
|
2
|
-
require '
|
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
|
13
|
-
#
|
14
|
-
#
|
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
|
-
|
20
|
-
|
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(
|
28
|
+
FileUtils.rm_rf(base_dir(options))
|
26
29
|
super
|
27
30
|
rescue Object
|
28
|
-
Logger.info
|
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
|
36
|
-
|
37
|
-
|
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
|
-
#
|
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.
|
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
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
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
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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.
|
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.
|
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
|
132
|
-
|
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
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
248
|
+
table = store.get_table(self.class)
|
267
249
|
transaction do |tx|
|
268
|
-
|
269
|
-
if cascade and #{klass}.
|
270
|
-
#{klass}.
|
271
|
-
|
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
|