og 0.20.0 → 0.21.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/CHANGELOG +796 -664
- data/INSTALL +24 -24
- data/README +39 -32
- data/Rakefile +41 -42
- data/benchmark/bench.rb +36 -36
- data/doc/AUTHORS +15 -12
- data/doc/LICENSE +3 -3
- data/doc/RELEASES +311 -243
- data/doc/config.txt +1 -1
- data/examples/mysql_to_psql.rb +15 -15
- data/examples/run.rb +92 -92
- data/install.rb +7 -17
- data/lib/og.rb +76 -75
- data/lib/og/collection.rb +203 -160
- data/lib/og/entity.rb +168 -169
- data/lib/og/errors.rb +5 -5
- data/lib/og/manager.rb +179 -178
- data/lib/og/mixin/hierarchical.rb +107 -107
- data/lib/og/mixin/optimistic_locking.rb +36 -36
- data/lib/og/mixin/orderable.rb +148 -148
- data/lib/og/mixin/timestamped.rb +8 -8
- data/lib/og/mixin/tree.rb +124 -124
- data/lib/og/relation.rb +237 -213
- data/lib/og/relation/belongs_to.rb +5 -5
- data/lib/og/relation/has_many.rb +60 -58
- data/lib/og/relation/joins_many.rb +93 -47
- data/lib/og/relation/refers_to.rb +25 -21
- data/lib/og/store.rb +210 -207
- data/lib/og/store/filesys.rb +79 -79
- data/lib/og/store/kirby.rb +263 -258
- data/lib/og/store/memory.rb +261 -261
- data/lib/og/store/mysql.rb +288 -284
- data/lib/og/store/psql.rb +261 -244
- data/lib/og/store/sql.rb +873 -720
- data/lib/og/store/sqlite.rb +177 -175
- data/lib/og/store/sqlserver.rb +204 -214
- data/lib/og/types.rb +1 -1
- data/lib/og/validation.rb +57 -57
- data/lib/vendor/mysql.rb +376 -376
- data/lib/vendor/mysql411.rb +10 -10
- data/test/og/mixin/tc_hierarchical.rb +59 -59
- data/test/og/mixin/tc_optimistic_locking.rb +40 -40
- data/test/og/mixin/tc_orderable.rb +67 -67
- data/test/og/mixin/tc_timestamped.rb +19 -19
- data/test/og/store/tc_filesys.rb +46 -46
- data/test/og/tc_inheritance.rb +81 -81
- data/test/og/tc_join.rb +67 -0
- data/test/og/tc_polymorphic.rb +49 -49
- data/test/og/tc_relation.rb +57 -30
- data/test/og/tc_select.rb +49 -0
- data/test/og/tc_store.rb +345 -337
- data/test/og/tc_types.rb +11 -11
- metadata +11 -18
data/lib/og/store/psql.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
begin
|
2
|
-
|
2
|
+
require 'postgres'
|
3
3
|
rescue Object => ex
|
4
|
-
|
5
|
-
|
4
|
+
Logger.error 'Ruby-PostgreSQL bindings are not installed!'
|
5
|
+
Logger.error ex
|
6
6
|
end
|
7
7
|
|
8
8
|
require 'og/store/sql'
|
@@ -11,58 +11,58 @@ require 'og/store/sql'
|
|
11
11
|
# more compatible with Og.
|
12
12
|
|
13
13
|
class PGresult
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
14
|
+
def blank?
|
15
|
+
0 == num_tuples
|
16
|
+
end
|
17
|
+
|
18
|
+
def next
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def each_row
|
23
|
+
for row in (0...num_tuples)
|
24
|
+
yield(self, row)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def first_value
|
29
|
+
val = getvalue(0, 0)
|
30
|
+
clear
|
31
|
+
return val
|
32
|
+
end
|
33
|
+
|
34
|
+
alias_method :close, :clear
|
35
35
|
end
|
36
36
|
|
37
37
|
module Og
|
38
38
|
|
39
39
|
module PsqlUtils
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
40
|
+
include SqlUtils
|
41
|
+
|
42
|
+
def escape(str)
|
43
|
+
return nil unless str
|
44
|
+
return PGconn.escape(str)
|
45
|
+
end
|
46
|
+
|
47
|
+
# TODO, mneumann:
|
48
|
+
#
|
49
|
+
# Blobs are actually a lot faster (and uses up less storage) for large data I
|
50
|
+
# think, as they need not to be encoded and decoded. I'd like to have both ;-)
|
51
|
+
# BYTEA is easier to handle than BLOBs, but if you implement BLOBs in a way
|
52
|
+
# that they are transparent to the user (as I did in Ruby/DBI), I'd prefer that
|
53
|
+
# way.
|
54
|
+
|
55
|
+
def blob(val)
|
56
|
+
val.gsub(/[\000-\037\047\134\177-\377]/) do |b|
|
57
|
+
"\\#{ b[0].to_s(8).rjust(3, '0') }"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def parse_blob(val)
|
62
|
+
val.gsub(/\\(\\|'|[0-3][0-7][0-7])/) do |s|
|
63
|
+
if s.size == 2 then s[1,1] else s[1,3].oct.chr end
|
64
|
+
end
|
65
|
+
end
|
66
66
|
|
67
67
|
end
|
68
68
|
|
@@ -76,206 +76,222 @@ end
|
|
76
76
|
# performance.
|
77
77
|
|
78
78
|
class PsqlStore < SqlStore
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
79
|
+
extend PsqlUtils
|
80
|
+
include PsqlUtils
|
81
|
+
|
82
|
+
def self.create(options)
|
83
|
+
# gmosx: system is used to avoid shell expansion.
|
84
|
+
system 'createdb', options[:name], '-U', options[:user]
|
85
|
+
super
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.destroy(options)
|
89
|
+
system 'dropdb', options[:name], '-U', options[:user]
|
90
|
+
super
|
91
|
+
end
|
92
|
+
|
93
|
+
def initialize(options)
|
94
|
+
super
|
95
|
+
|
96
|
+
@typemap.update(Og::Blob => 'bytea')
|
97
|
+
|
98
|
+
@conn = PGconn.connect(
|
99
|
+
options[:address],
|
100
|
+
options[:port], nil, nil,
|
101
|
+
options[:name],
|
102
|
+
options[:user].to_s,
|
103
|
+
options[:password].to_s
|
104
|
+
)
|
105
|
+
|
106
|
+
schema_order = options[:schema_order]
|
107
|
+
encoding = options[:encoding]
|
108
|
+
min_messages = options[:min_messages]
|
109
|
+
|
110
|
+
@conn.exec("SET search_path TO #{schema_order}") if schema_order
|
109
111
|
@conn.exec("SET client_encoding TO '#{encoding}'") if encoding
|
110
112
|
@conn.exec("SET client_min_messages TO '#{min_messages}'") if min_messages
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
113
|
+
rescue => ex
|
114
|
+
# gmosx: any idea how to better test this?
|
115
|
+
if ex.to_s =~ /database .* does not exist/i
|
116
|
+
Logger.info "Database '#{options[:name]}' not found!"
|
117
|
+
self.class.create(options)
|
118
|
+
retry
|
119
|
+
end
|
120
|
+
raise
|
121
|
+
end
|
122
|
+
|
123
|
+
def close
|
124
|
+
@conn.close
|
125
|
+
super
|
126
|
+
end
|
127
|
+
|
128
|
+
def enchant(klass, manager)
|
129
|
+
klass.const_set 'OGSEQ', "#{table(klass)}_oid_seq"
|
130
|
+
klass.property :oid, Fixnum, :sql => 'serial PRIMARY KEY'
|
131
|
+
super
|
132
|
+
end
|
133
|
+
|
134
|
+
def query(sql)
|
135
|
+
Logger.debug sql if $DBG
|
136
|
+
return @conn.exec(sql)
|
137
|
+
rescue => ex
|
138
|
+
handle_sql_exception(ex, sql)
|
139
|
+
end
|
140
|
+
|
141
|
+
def exec(sql)
|
142
|
+
Logger.debug sql if $DBG
|
143
|
+
@conn.exec(sql).clear
|
144
|
+
rescue => ex
|
145
|
+
handle_sql_exception(ex, sql)
|
146
|
+
end
|
147
|
+
|
148
|
+
def sql_update(sql)
|
149
|
+
Logger.debug sql if $DBG
|
150
|
+
res = @conn.exec(sql)
|
151
|
+
changed = res.cmdtuples
|
152
|
+
res.clear
|
153
|
+
changed
|
154
|
+
end
|
155
|
+
|
156
|
+
# Start a new transaction.
|
157
|
+
|
158
|
+
def start
|
159
|
+
# neumann: works with earlier PSQL databases too.
|
160
|
+
exec('BEGIN TRANSACTION') if @transaction_nesting < 1
|
161
|
+
@transaction_nesting += 1
|
162
|
+
end
|
153
163
|
|
154
164
|
private
|
155
165
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
166
|
+
def create_table(klass)
|
167
|
+
fields = fields_for_class(klass)
|
168
|
+
|
169
|
+
sql = "CREATE TABLE #{klass::OGTABLE} (#{fields.join(', ')}"
|
170
|
+
|
171
|
+
# Create table constrains.
|
172
|
+
|
173
|
+
if klass.__meta and constrains = klass.__meta[:sql_constrain]
|
174
|
+
sql << ", #{constrains.join(', ')}"
|
175
|
+
end
|
176
|
+
|
177
|
+
sql << ") WITHOUT OIDS;"
|
178
|
+
|
179
|
+
# Create indices.
|
180
|
+
|
181
|
+
if klass.__meta and indices = klass.__meta[:index]
|
182
|
+
for data in indices
|
183
|
+
idx, options = *data
|
184
|
+
idx = idx.to_s
|
185
|
+
pre_sql, post_sql = options[:pre], options[:post]
|
186
|
+
idxname = idx.gsub(/ /, "").gsub(/,/, "_").gsub(/\(.*\)/, "")
|
187
|
+
sql << " CREATE #{pre_sql} INDEX #{klass::OGTABLE}_#{idxname}_idx #{post_sql} ON #{klass::OGTABLE} (#{idx});"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
begin
|
192
|
+
@conn.exec(sql).clear
|
193
|
+
Logger.info "Created table '#{klass::OGTABLE}'."
|
194
|
+
rescue Object => ex
|
195
|
+
# gmosx: any idea how to better test this?
|
196
|
+
if ex.to_s =~ /relation .* already exists/i
|
197
|
+
Logger.debug 'Table already exists' if $DBG
|
198
|
+
return
|
199
|
+
else
|
200
|
+
raise
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Create join tables if needed. Join tables are used in
|
205
|
+
# 'many_to_many' relations.
|
206
|
+
|
207
|
+
if klass.__meta and join_tables = klass.__meta[:join_tables]
|
208
|
+
for info in join_tables
|
209
|
+
begin
|
210
|
+
create_join_table_sql(info).each do |sql|
|
211
|
+
@conn.exec(sql).clear
|
212
|
+
end
|
213
|
+
rescue Object => ex
|
214
|
+
# gmosx: any idea how to better test this?
|
215
|
+
if ex.to_s =~ /relation .* already exists/i
|
216
|
+
Logger.debug 'Join table already exists' if $DBG
|
217
|
+
else
|
218
|
+
raise
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def drop_table(klass)
|
226
|
+
super
|
227
|
+
exec "DROP SEQUENCE #{klass::OGSEQ}"
|
228
|
+
end
|
229
|
+
|
230
|
+
def create_field_map(klass)
|
231
|
+
res = @conn.exec "SELECT * FROM #{klass::OGTABLE} LIMIT 1"
|
232
|
+
map = {}
|
233
|
+
|
234
|
+
for field in res.fields
|
235
|
+
map[field.intern] = res.fieldnum(field)
|
236
|
+
end
|
237
|
+
|
238
|
+
return map
|
239
|
+
ensure
|
240
|
+
res.clear if res
|
241
|
+
end
|
242
|
+
|
243
|
+
def read_prop(p, col)
|
244
|
+
if p.klass.ancestors.include?(Integer)
|
245
|
+
return "#{self.class}.parse_int(res.getvalue(row, #{col} + offset))"
|
246
|
+
elsif p.klass.ancestors.include?(Float)
|
247
|
+
return "#{self.class}.parse_float(res.getvalue(row, #{col} + offset))"
|
248
|
+
elsif p.klass.ancestors.include?(String)
|
249
|
+
return "res.getvalue(row, #{col} + offset)"
|
250
|
+
elsif p.klass.ancestors.include?(Time)
|
251
|
+
return "#{self.class}.parse_timestamp(res.getvalue(row, #{col} + offset))"
|
252
|
+
elsif p.klass.ancestors.include?(Date)
|
253
|
+
return "#{self.class}.parse_date(res.getvalue(row, #{col} + offset))"
|
254
|
+
elsif p.klass.ancestors.include?(TrueClass)
|
255
|
+
return %|('t' == res.getvalue(row, #{col} + offset))|
|
246
256
|
elsif p.klass.ancestors.include?(Og::Blob)
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
257
|
+
return "#{self.class}.parse_blob(res.getvalue(row, #{col} + offset))"
|
258
|
+
else
|
259
|
+
return "YAML.load(res.getvalue(row, #{col} + offset))"
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
#--
|
264
|
+
# TODO: create stored procedure.
|
265
|
+
#++
|
266
|
+
|
267
|
+
def eval_og_insert(klass)
|
268
|
+
props = klass.properties
|
269
|
+
values = props.collect { |p| write_prop(p) }.join(',')
|
270
|
+
|
271
|
+
if klass.metadata.superclass or klass.metadata.subclasses
|
272
|
+
props << Property.new(:ogtype, String)
|
273
|
+
values << ", '#{klass}'"
|
274
|
+
end
|
275
|
+
|
276
|
+
sql = "INSERT INTO #{klass::OGTABLE} (#{props.collect {|p| p.symbol.to_s}.join(',')}) VALUES (#{values})"
|
277
|
+
|
278
|
+
klass.class_eval %{
|
279
|
+
def og_insert(store)
|
280
|
+
#{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
|
281
|
+
res = store.conn.exec "SELECT nextval('#{klass::OGSEQ}')"
|
282
|
+
@#{klass.pk_symbol} = res.getvalue(0, 0).to_i
|
283
|
+
res.clear
|
284
|
+
store.conn.exec("#{sql}").clear
|
285
|
+
#{Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
|
286
|
+
end
|
287
|
+
}
|
288
|
+
end
|
289
|
+
|
290
|
+
def read_row(obj, res, res_row, row)
|
291
|
+
res.fields.each_with_index do |field, idx|
|
292
|
+
obj.instance_variable_set "@#{field}", res.getvalue(row, idx)
|
293
|
+
end
|
294
|
+
end
|
279
295
|
|
280
296
|
end
|
281
297
|
|
@@ -283,3 +299,4 @@ end
|
|
283
299
|
|
284
300
|
# * George Moschovitis <gm@navel.gr>
|
285
301
|
# * Michael Neumann <mneumann@ntecs.de>
|
302
|
+
# * Ysabel <deb@ysabel.org>
|