og 0.16.0 → 0.17.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 +485 -0
- data/README +35 -12
- data/Rakefile +4 -7
- data/benchmark/bench.rb +1 -1
- data/doc/AUTHORS +3 -3
- data/doc/RELEASES +153 -2
- data/doc/config.txt +0 -7
- data/doc/tutorial.txt +7 -0
- data/examples/README +5 -0
- data/examples/mysql_to_psql.rb +25 -50
- data/examples/run.rb +62 -77
- data/install.rb +1 -1
- data/lib/og.rb +45 -106
- data/lib/og/collection.rb +156 -0
- data/lib/og/entity.rb +131 -0
- data/lib/og/errors.rb +10 -15
- data/lib/og/manager.rb +115 -0
- data/lib/og/{mixins → mixin}/hierarchical.rb +43 -37
- data/lib/og/{mixins → mixin}/orderable.rb +35 -35
- data/lib/og/{mixins → mixin}/timestamped.rb +0 -6
- data/lib/og/{mixins → mixin}/tree.rb +0 -4
- data/lib/og/relation.rb +178 -0
- data/lib/og/relation/belongs_to.rb +14 -0
- data/lib/og/relation/has_many.rb +62 -0
- data/lib/og/relation/has_one.rb +17 -0
- data/lib/og/relation/joins_many.rb +69 -0
- data/lib/og/relation/many_to_many.rb +17 -0
- data/lib/og/relation/refers_to.rb +31 -0
- data/lib/og/store.rb +223 -0
- data/lib/og/store/filesys.rb +113 -0
- data/lib/og/store/madeleine.rb +4 -0
- data/lib/og/store/memory.rb +291 -0
- data/lib/og/store/mysql.rb +283 -0
- data/lib/og/store/psql.rb +238 -0
- data/lib/og/store/sql.rb +599 -0
- data/lib/og/store/sqlite.rb +190 -0
- data/lib/og/store/sqlserver.rb +262 -0
- data/lib/og/types.rb +19 -0
- data/lib/og/validation.rb +0 -4
- data/test/og/{mixins → mixin}/tc_hierarchical.rb +21 -23
- data/test/og/{mixins → mixin}/tc_orderable.rb +15 -14
- data/test/og/mixin/tc_timestamped.rb +38 -0
- data/test/og/store/tc_filesys.rb +71 -0
- data/test/og/tc_relation.rb +36 -0
- data/test/og/tc_store.rb +290 -0
- data/test/og/tc_types.rb +21 -0
- metadata +54 -40
- data/examples/mock_example.rb +0 -50
- data/lib/og/adapters/base.rb +0 -706
- data/lib/og/adapters/filesys.rb +0 -117
- data/lib/og/adapters/mysql.rb +0 -350
- data/lib/og/adapters/oracle.rb +0 -368
- data/lib/og/adapters/psql.rb +0 -272
- data/lib/og/adapters/sqlite.rb +0 -265
- data/lib/og/adapters/sqlserver.rb +0 -356
- data/lib/og/database.rb +0 -290
- data/lib/og/enchant.rb +0 -149
- data/lib/og/meta.rb +0 -407
- data/lib/og/testing/mock.rb +0 -165
- data/lib/og/typemacros.rb +0 -24
- data/test/og/adapters/tc_filesys.rb +0 -83
- data/test/og/adapters/tc_sqlite.rb +0 -86
- data/test/og/adapters/tc_sqlserver.rb +0 -96
- data/test/og/tc_automanage.rb +0 -46
- data/test/og/tc_lifecycle.rb +0 -105
- data/test/og/tc_many_to_many.rb +0 -61
- data/test/og/tc_meta.rb +0 -55
- data/test/og/tc_validation.rb +0 -89
- data/test/tc_og.rb +0 -364
@@ -0,0 +1,190 @@
|
|
1
|
+
begin
|
2
|
+
require 'sqlite3'
|
3
|
+
rescue Object => ex
|
4
|
+
Logger.error 'Ruby-Sqlite3 bindings are not installed!'
|
5
|
+
Logger.error ex
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'fileutils'
|
9
|
+
|
10
|
+
require 'og/store/sql'
|
11
|
+
|
12
|
+
# Customize the standard postgres resultset to make
|
13
|
+
# more compatible with Og.
|
14
|
+
|
15
|
+
class SQLite3::ResultSet
|
16
|
+
def blank?
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
def each_row
|
21
|
+
each do |row|
|
22
|
+
yield(row, 0)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def first_value
|
27
|
+
val = self.next[0]
|
28
|
+
close
|
29
|
+
return val
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module Og
|
34
|
+
|
35
|
+
# A Store that persists objects into an Sqlite3 database.
|
36
|
+
# To read documentation about the methods, consult the documentation
|
37
|
+
# for SqlStore and Store.
|
38
|
+
|
39
|
+
class SqliteStore < SqlStore
|
40
|
+
|
41
|
+
def self.destroy(options)
|
42
|
+
begin
|
43
|
+
FileUtils.rm("#{options[:name]}.db")
|
44
|
+
super
|
45
|
+
rescue Object
|
46
|
+
Logger.info "Cannot drop '#{options[:name]}'!"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def initialize(options)
|
51
|
+
super
|
52
|
+
@conn = SQLite3::Database.new("#{options[:name]}.db")
|
53
|
+
end
|
54
|
+
|
55
|
+
def close
|
56
|
+
# FIXME: problems when closing due to unfinalised statements.
|
57
|
+
# @conn.close
|
58
|
+
super
|
59
|
+
end
|
60
|
+
|
61
|
+
def enchant(klass, manager)
|
62
|
+
klass.property :oid, Fixnum, :sql => 'integer PRIMARY KEY'
|
63
|
+
super
|
64
|
+
end
|
65
|
+
|
66
|
+
def query(sql)
|
67
|
+
Logger.debug sql if $DBG
|
68
|
+
return @conn.query(sql)
|
69
|
+
rescue => ex
|
70
|
+
handle_sql_exception(ex, sql)
|
71
|
+
end
|
72
|
+
|
73
|
+
def exec(sql)
|
74
|
+
Logger.debug sql if $DBG
|
75
|
+
@conn.query(sql).close
|
76
|
+
rescue => ex
|
77
|
+
handle_sql_exception(ex, sql)
|
78
|
+
end
|
79
|
+
|
80
|
+
def start
|
81
|
+
@conn.transaction if @transaction_nesting < 1
|
82
|
+
@transaction_nesting += 1
|
83
|
+
end
|
84
|
+
|
85
|
+
def commit
|
86
|
+
@transaction_nesting -= 1
|
87
|
+
@conn.commit if @transaction_nesting < 1
|
88
|
+
end
|
89
|
+
|
90
|
+
def rollback
|
91
|
+
@transaction_nesting -= 1
|
92
|
+
@conn.rollback if @transaction_nesting < 1
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def create_table(klass)
|
98
|
+
columns = columns_for_class(klass)
|
99
|
+
|
100
|
+
sql = "CREATE TABLE #{klass::OGTABLE} (#{columns.join(', ')}"
|
101
|
+
|
102
|
+
# Create table constrains.
|
103
|
+
|
104
|
+
if klass.__meta and constrains = klass.__meta[:sql_constrain]
|
105
|
+
sql << ", #{constrains.join(', ')}"
|
106
|
+
end
|
107
|
+
|
108
|
+
sql << ");"
|
109
|
+
|
110
|
+
# Create indices.
|
111
|
+
|
112
|
+
if klass.__meta and indices = klass.__meta[:index]
|
113
|
+
for data in indices
|
114
|
+
idx, options = *data
|
115
|
+
idx = idx.to_s
|
116
|
+
pre_sql, post_sql = options[:pre], options[:post]
|
117
|
+
idxname = idx.gsub(/ /, "").gsub(/,/, "_").gsub(/\(.*\)/, "")
|
118
|
+
sql << " CREATE #{pre_sql} INDEX #{klass::OGTABLE}_#{idxname}_idx #{post_sql} ON #{klass::OGTABLE} (#{idx});"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
begin
|
123
|
+
@conn.query(sql).close
|
124
|
+
Logger.info "Created table '#{klass::OGTABLE}'."
|
125
|
+
rescue Object => ex
|
126
|
+
# gmosx: any idea how to better test this?
|
127
|
+
if ex.to_s =~ /table .* already exists/i
|
128
|
+
Logger.debug 'Table already exists' if $DBG
|
129
|
+
return
|
130
|
+
else
|
131
|
+
raise
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Create join tables if needed. Join tables are used in
|
136
|
+
# 'many_to_many' relations.
|
137
|
+
|
138
|
+
if klass.__meta and join_tables = klass.__meta[:join_tables]
|
139
|
+
for join_table in join_tables
|
140
|
+
begin
|
141
|
+
@conn.query("CREATE TABLE #{join_table} (key1 integer NOT NULL, key2 integer NOT NULL)").close
|
142
|
+
@conn.query("CREATE INDEX #{join_table}_key1_idx ON #{join_table} (key1)").close
|
143
|
+
@conn.query("CREATE INDEX #{join_table}_key2_idx ON #{join_table} (key2)").close
|
144
|
+
rescue Object => ex
|
145
|
+
# gmosx: any idea how to better test this?
|
146
|
+
if ex.to_s =~ /table .* already exists/i
|
147
|
+
Logger.debug 'Join table already exists' if $DBG
|
148
|
+
else
|
149
|
+
raise
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def create_column_map(klass)
|
157
|
+
res = @conn.query "SELECT * FROM #{klass::OGTABLE} LIMIT 1"
|
158
|
+
map = {}
|
159
|
+
|
160
|
+
columns = res.columns
|
161
|
+
|
162
|
+
columns.size.times do |i|
|
163
|
+
map[columns[i].intern] = i
|
164
|
+
end
|
165
|
+
|
166
|
+
return map
|
167
|
+
ensure
|
168
|
+
res.close if res
|
169
|
+
end
|
170
|
+
|
171
|
+
def eval_og_insert(klass)
|
172
|
+
pk = klass.primary_key.first
|
173
|
+
props = klass.properties
|
174
|
+
values = props.collect { |p| write_prop(p) }.join(',')
|
175
|
+
|
176
|
+
sql = "INSERT INTO #{klass::OGTABLE} (#{props.collect {|p| p.symbol.to_s}.join(',')}) VALUES (#{values})"
|
177
|
+
|
178
|
+
klass.class_eval %{
|
179
|
+
def og_insert(store)
|
180
|
+
#{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
|
181
|
+
store.conn.query("#{sql}").close
|
182
|
+
@#{pk} = store.conn.last_insert_row_id
|
183
|
+
#{Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
|
184
|
+
end
|
185
|
+
}
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|
@@ -0,0 +1,262 @@
|
|
1
|
+
begin
|
2
|
+
require 'dbi'
|
3
|
+
rescue Object => ex
|
4
|
+
Logger.error 'Ruby-DBI bindings not present or ADO support not available.'
|
5
|
+
Logger.error ex
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'og/store/sql'
|
9
|
+
|
10
|
+
# Customize the standard SqlServer resultset to make
|
11
|
+
# more compatible with Og.
|
12
|
+
=begin
|
13
|
+
class Sqlserver::Result
|
14
|
+
def blank?
|
15
|
+
0 == num_rows
|
16
|
+
end
|
17
|
+
|
18
|
+
alias_method :next, :fetch_row
|
19
|
+
|
20
|
+
def each_row
|
21
|
+
each do |row|
|
22
|
+
yield(row, 0)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def first_value
|
27
|
+
val = fetch_row[0]
|
28
|
+
free
|
29
|
+
return val
|
30
|
+
end
|
31
|
+
|
32
|
+
alias_method :close, :free
|
33
|
+
end
|
34
|
+
=end
|
35
|
+
|
36
|
+
module Og
|
37
|
+
|
38
|
+
module SqlserverUtils
|
39
|
+
include SqlUtils
|
40
|
+
|
41
|
+
def escape(str)
|
42
|
+
return nil unless str
|
43
|
+
return Sqlserver.quote(str)
|
44
|
+
end
|
45
|
+
|
46
|
+
def quote(val)
|
47
|
+
case val
|
48
|
+
when Fixnum, Integer, Float
|
49
|
+
val ? val.to_s : 'NULL'
|
50
|
+
when String
|
51
|
+
val ? "'#{escape(val)}'" : 'NULL'
|
52
|
+
when Time
|
53
|
+
val ? "'#{timestamp(val)}'" : 'NULL'
|
54
|
+
when Date
|
55
|
+
val ? "'#{date(val)}'" : 'NULL'
|
56
|
+
when TrueClass
|
57
|
+
val ? "'1'" : 'NULL'
|
58
|
+
else
|
59
|
+
# gmosx: keep the '' for nil symbols.
|
60
|
+
val ? escape(val.to_yaml) : ''
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# A Store that persists objects into a Sqlserver database.
|
66
|
+
# To read documentation about the methods, consult the documentation
|
67
|
+
# for SqlStore and Store.
|
68
|
+
|
69
|
+
class SqlserverStore < SqlStore
|
70
|
+
extend SqlserverUtils
|
71
|
+
include SqlserverUtils
|
72
|
+
|
73
|
+
def self.create(options)
|
74
|
+
raise 'Not implemented'
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.destroy(options)
|
78
|
+
raise 'Not implemented'
|
79
|
+
end
|
80
|
+
|
81
|
+
def initialize(options)
|
82
|
+
super
|
83
|
+
|
84
|
+
begin
|
85
|
+
@conn = DBI.connect("DBI:ADO:Provider=SQLOLEDB;Data Source=#{options[:address]};Initial Catalog=#{options[:name]};User Id=#{options[:user]};Password=#{options[:password]};")
|
86
|
+
rescue => ex
|
87
|
+
# gmosx, FIXME: drak, fix this!
|
88
|
+
if ex.to_s =~ /database .* does not exist/i
|
89
|
+
Logger.info "Database '#{options[:name]}' not found!"
|
90
|
+
self.class.create(options)
|
91
|
+
retry
|
92
|
+
end
|
93
|
+
raise
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def close
|
98
|
+
@conn.disconnect
|
99
|
+
super
|
100
|
+
end
|
101
|
+
|
102
|
+
def enchant(klass, manager)
|
103
|
+
klass.property :oid, Fixnum, :sql => 'integer AUTO_INCREMENT PRIMARY KEY'
|
104
|
+
super
|
105
|
+
end
|
106
|
+
|
107
|
+
def query(sql)
|
108
|
+
# Logger.debug sql if $DBG
|
109
|
+
return @conn.select_all(sql)
|
110
|
+
rescue => ex
|
111
|
+
handle_sql_exception(ex, sql)
|
112
|
+
end
|
113
|
+
|
114
|
+
def exec(sql)
|
115
|
+
# Logger.debug sql if $DBG
|
116
|
+
@conn.execute(sql).finish
|
117
|
+
rescue => ex
|
118
|
+
handle_sql_exception(ex, sql)
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def create_table(klass)
|
124
|
+
columns = columns_for_class(klass)
|
125
|
+
|
126
|
+
sql = "CREATE TABLE #{klass::OGTABLE} (#{columns.join(', ')}"
|
127
|
+
|
128
|
+
# Create table constrains.
|
129
|
+
|
130
|
+
if klass.__meta and constrains = klass.__meta[:sql_constrain]
|
131
|
+
sql << ", #{constrains.join(', ')}"
|
132
|
+
end
|
133
|
+
|
134
|
+
sql << ");"
|
135
|
+
|
136
|
+
# Create indices.
|
137
|
+
|
138
|
+
if klass.__meta and indices = klass.__meta[:index]
|
139
|
+
for data in indices
|
140
|
+
idx, options = *data
|
141
|
+
idx = idx.to_s
|
142
|
+
pre_sql, post_sql = options[:pre], options[:post]
|
143
|
+
idxname = idx.gsub(/ /, "").gsub(/,/, "_").gsub(/\(.*\)/, "")
|
144
|
+
sql << " CREATE #{pre_sql} INDEX #{klass::OGTABLE}_#{idxname}_idx #{post_sql} ON #{klass::OGTABLE} (#{idx});"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
conn.query_with_result = false
|
149
|
+
|
150
|
+
begin
|
151
|
+
conn.query(sql)
|
152
|
+
Logger.info "Created table '#{klass::OGTABLE}'."
|
153
|
+
rescue => ex
|
154
|
+
# gmosx: any idea how to better test this?
|
155
|
+
if ex.errno == 1050 # table already exists.
|
156
|
+
Logger.debug 'Table already exists' if $DBG
|
157
|
+
return
|
158
|
+
else
|
159
|
+
raise
|
160
|
+
end
|
161
|
+
end
|
162
|
+
=begin
|
163
|
+
# Create join tables if needed. Join tables are used in
|
164
|
+
# 'many_to_many' relations.
|
165
|
+
|
166
|
+
if klass.__meta and joins = klass.__meta[:sql_join]
|
167
|
+
for data in joins
|
168
|
+
# the class to join to and some options.
|
169
|
+
join_name, join_class, options = *data
|
170
|
+
|
171
|
+
join_table = "#{self.class.join_table(klass, join_class, join_name)}"
|
172
|
+
join_src = "#{self.class.encode(klass)}_oid"
|
173
|
+
join_dst = "#{self.class.encode(join_class)}_oid"
|
174
|
+
|
175
|
+
begin
|
176
|
+
conn.exec("CREATE TABLE #{join_table} ( key1 integer NOT NULL, key2 integer NOT NULL )").clear
|
177
|
+
conn.exec("CREATE INDEX #{join_table}_key1_idx ON #{join_table} (key1)").clear
|
178
|
+
conn.exec("CREATE INDEX #{join_table}_key2_idx ON #{join_table} (key2)").clear
|
179
|
+
rescue => ex
|
180
|
+
# gmosx: any idea how to better test this?
|
181
|
+
if ex.errno == 1050 # table already exists.
|
182
|
+
Logger.debug 'Join table already exists'
|
183
|
+
else
|
184
|
+
raise
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
=end
|
190
|
+
end
|
191
|
+
|
192
|
+
def create_column_map(klass)
|
193
|
+
conn.query_with_result = true
|
194
|
+
res = @conn.query "SELECT * FROM #{klass::OGTABLE} LIMIT 1"
|
195
|
+
map = {}
|
196
|
+
|
197
|
+
res.num_fields.times do |i|
|
198
|
+
map[res.fetch_field.name.intern] = i
|
199
|
+
end
|
200
|
+
|
201
|
+
return map
|
202
|
+
ensure
|
203
|
+
res.close if res
|
204
|
+
end
|
205
|
+
|
206
|
+
def write_prop(p)
|
207
|
+
if p.klass.ancestors.include?(Integer)
|
208
|
+
return "#\{@#{p.symbol} || 'NULL'\}"
|
209
|
+
elsif p.klass.ancestors.include?(Float)
|
210
|
+
return "#\{@#{p.symbol} || 'NULL'\}"
|
211
|
+
elsif p.klass.ancestors.include?(String)
|
212
|
+
return %|#\{@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol})\}'" : 'NULL'\}|
|
213
|
+
elsif p.klass.ancestors.include?(Time)
|
214
|
+
return %|#\{@#{p.symbol} ? "'#\{#{self.class}.timestamp(@#{p.symbol})\}'" : 'NULL'\}|
|
215
|
+
elsif p.klass.ancestors.include?(Date)
|
216
|
+
return %|#\{@#{p.symbol} ? "'#\{#{self.class}.date(@#{p.symbol})\}'" : 'NULL'\}|
|
217
|
+
elsif p.klass.ancestors.include?(TrueClass)
|
218
|
+
return "#\{@#{p.symbol} ? \"'1'\" : 'NULL' \}"
|
219
|
+
else
|
220
|
+
# gmosx: keep the '' for nil symbols.
|
221
|
+
return %|#\{@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol}.to_yaml)\}'" : "''"\}|
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def read_prop(p, col)
|
226
|
+
if p.klass.ancestors.include?(Integer)
|
227
|
+
return "res[#{col} + offset].to_i"
|
228
|
+
elsif p.klass.ancestors.include?(Float)
|
229
|
+
return "res[#{col} + offset].to_f"
|
230
|
+
elsif p.klass.ancestors.include?(String)
|
231
|
+
return "res[#{col} + offset]"
|
232
|
+
elsif p.klass.ancestors.include?(Time)
|
233
|
+
return "#{self.class}.parse_timestamp(res[#{col} + offset])"
|
234
|
+
elsif p.klass.ancestors.include?(Date)
|
235
|
+
return "#{self.class}.parse_date(res[#{col} + offset])"
|
236
|
+
elsif p.klass.ancestors.include?(TrueClass)
|
237
|
+
return "('0' != res[#{col} + offset])"
|
238
|
+
else
|
239
|
+
return "YAML.load(res[#{col} + offset])"
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def eval_og_insert(klass)
|
244
|
+
props = klass.properties
|
245
|
+
values = props.collect { |p| write_prop(p) }.join(',')
|
246
|
+
|
247
|
+
sql = "INSERT INTO #{klass::OGTABLE} (#{props.collect {|p| p.symbol.to_s}.join(',')}) VALUES (#{values})"
|
248
|
+
|
249
|
+
klass.class_eval %{
|
250
|
+
def og_insert(store)
|
251
|
+
#{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
|
252
|
+
store.conn.query_with_result = false
|
253
|
+
store.conn.query "#{sql}"
|
254
|
+
@#{klass.pk_symbol} = store.conn.insert_id
|
255
|
+
#{Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
|
256
|
+
end
|
257
|
+
}
|
258
|
+
end
|
259
|
+
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|