og 0.16.0 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/CHANGELOG +485 -0
  2. data/README +35 -12
  3. data/Rakefile +4 -7
  4. data/benchmark/bench.rb +1 -1
  5. data/doc/AUTHORS +3 -3
  6. data/doc/RELEASES +153 -2
  7. data/doc/config.txt +0 -7
  8. data/doc/tutorial.txt +7 -0
  9. data/examples/README +5 -0
  10. data/examples/mysql_to_psql.rb +25 -50
  11. data/examples/run.rb +62 -77
  12. data/install.rb +1 -1
  13. data/lib/og.rb +45 -106
  14. data/lib/og/collection.rb +156 -0
  15. data/lib/og/entity.rb +131 -0
  16. data/lib/og/errors.rb +10 -15
  17. data/lib/og/manager.rb +115 -0
  18. data/lib/og/{mixins → mixin}/hierarchical.rb +43 -37
  19. data/lib/og/{mixins → mixin}/orderable.rb +35 -35
  20. data/lib/og/{mixins → mixin}/timestamped.rb +0 -6
  21. data/lib/og/{mixins → mixin}/tree.rb +0 -4
  22. data/lib/og/relation.rb +178 -0
  23. data/lib/og/relation/belongs_to.rb +14 -0
  24. data/lib/og/relation/has_many.rb +62 -0
  25. data/lib/og/relation/has_one.rb +17 -0
  26. data/lib/og/relation/joins_many.rb +69 -0
  27. data/lib/og/relation/many_to_many.rb +17 -0
  28. data/lib/og/relation/refers_to.rb +31 -0
  29. data/lib/og/store.rb +223 -0
  30. data/lib/og/store/filesys.rb +113 -0
  31. data/lib/og/store/madeleine.rb +4 -0
  32. data/lib/og/store/memory.rb +291 -0
  33. data/lib/og/store/mysql.rb +283 -0
  34. data/lib/og/store/psql.rb +238 -0
  35. data/lib/og/store/sql.rb +599 -0
  36. data/lib/og/store/sqlite.rb +190 -0
  37. data/lib/og/store/sqlserver.rb +262 -0
  38. data/lib/og/types.rb +19 -0
  39. data/lib/og/validation.rb +0 -4
  40. data/test/og/{mixins → mixin}/tc_hierarchical.rb +21 -23
  41. data/test/og/{mixins → mixin}/tc_orderable.rb +15 -14
  42. data/test/og/mixin/tc_timestamped.rb +38 -0
  43. data/test/og/store/tc_filesys.rb +71 -0
  44. data/test/og/tc_relation.rb +36 -0
  45. data/test/og/tc_store.rb +290 -0
  46. data/test/og/tc_types.rb +21 -0
  47. metadata +54 -40
  48. data/examples/mock_example.rb +0 -50
  49. data/lib/og/adapters/base.rb +0 -706
  50. data/lib/og/adapters/filesys.rb +0 -117
  51. data/lib/og/adapters/mysql.rb +0 -350
  52. data/lib/og/adapters/oracle.rb +0 -368
  53. data/lib/og/adapters/psql.rb +0 -272
  54. data/lib/og/adapters/sqlite.rb +0 -265
  55. data/lib/og/adapters/sqlserver.rb +0 -356
  56. data/lib/og/database.rb +0 -290
  57. data/lib/og/enchant.rb +0 -149
  58. data/lib/og/meta.rb +0 -407
  59. data/lib/og/testing/mock.rb +0 -165
  60. data/lib/og/typemacros.rb +0 -24
  61. data/test/og/adapters/tc_filesys.rb +0 -83
  62. data/test/og/adapters/tc_sqlite.rb +0 -86
  63. data/test/og/adapters/tc_sqlserver.rb +0 -96
  64. data/test/og/tc_automanage.rb +0 -46
  65. data/test/og/tc_lifecycle.rb +0 -105
  66. data/test/og/tc_many_to_many.rb +0 -61
  67. data/test/og/tc_meta.rb +0 -55
  68. data/test/og/tc_validation.rb +0 -89
  69. data/test/tc_og.rb +0 -364
@@ -0,0 +1,283 @@
1
+ begin
2
+ require 'mysql'
3
+ rescue Object => ex
4
+ Logger.error 'Ruby-Mysql bindings are not installed!'
5
+ Logger.error ex
6
+ end
7
+
8
+ require 'og/store/sql'
9
+
10
+ # Customize the standard mysql resultset to make
11
+ # more compatible with Og.
12
+
13
+ class Mysql::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
+
35
+ module Og
36
+
37
+ module MysqlUtils
38
+ include SqlUtils
39
+
40
+ def escape(str)
41
+ return nil unless str
42
+ return Mysql.quote(str)
43
+ end
44
+
45
+ def quote(val)
46
+ case val
47
+ when Fixnum, Integer, Float
48
+ val ? val.to_s : 'NULL'
49
+ when String
50
+ val ? "'#{escape(val)}'" : 'NULL'
51
+ when Time
52
+ val ? "'#{timestamp(val)}'" : 'NULL'
53
+ when Date
54
+ val ? "'#{date(val)}'" : 'NULL'
55
+ when TrueClass
56
+ val ? "'1'" : 'NULL'
57
+ else
58
+ # gmosx: keep the '' for nil symbols.
59
+ val ? escape(val.to_yaml) : ''
60
+ end
61
+ end
62
+ end
63
+
64
+ # A Store that persists objects into a MySQL database.
65
+ # To read documentation about the methods, consult the documentation
66
+ # for SqlStore and Store.
67
+
68
+ class MysqlStore < SqlStore
69
+ extend MysqlUtils
70
+ include MysqlUtils
71
+
72
+ def self.create(options)
73
+ # gmosx: system is used to avoid shell expansion.
74
+ system 'mysqladmin', '-f', "--user=#{options[:user]}",
75
+ "--password=#{options[:password]}",
76
+ 'create', options[:name]
77
+ super
78
+ end
79
+
80
+ def self.destroy(options)
81
+ system 'mysqladmin', '-f', "--user=#{options[:user]}",
82
+ "--password=#{options[:password]}", 'drop',
83
+ options[:name]
84
+ super
85
+ end
86
+
87
+ def initialize(options)
88
+ super
89
+
90
+ @typemap.update(TrueClass => 'tinyint')
91
+
92
+ @conn = Mysql.connect(
93
+ options[:address] || 'localhost',
94
+ options[:user],
95
+ options[:password],
96
+ options[:name]
97
+ )
98
+ rescue => ex
99
+ if ex.errno == 1049 # database does not exist.
100
+ Logger.info "Database '#{options[:name]}' not found!"
101
+ self.class.create(options)
102
+ retry
103
+ end
104
+ raise
105
+ end
106
+
107
+ def close
108
+ @conn.close
109
+ super
110
+ end
111
+
112
+ def enchant(klass, manager)
113
+ klass.property :oid, Fixnum, :sql => 'integer AUTO_INCREMENT PRIMARY KEY'
114
+ super
115
+ end
116
+
117
+ def query(sql)
118
+ # Logger.debug sql if $DBG
119
+ @conn.query_with_result = true
120
+ return @conn.query(sql)
121
+ rescue => ex
122
+ handle_sql_exception(ex, sql)
123
+ end
124
+
125
+ def exec(sql)
126
+ # Logger.debug sql if $DBG
127
+ @conn.query_with_result = false
128
+ @conn.query(sql)
129
+ rescue => ex
130
+ handle_sql_exception(ex, sql)
131
+ end
132
+
133
+ def start
134
+ # nop
135
+ # FIXME: InnoDB supports transactions.
136
+ end
137
+
138
+ # Commit a transaction.
139
+
140
+ def commit
141
+ # nop, not supported?
142
+ # FIXME: InnoDB supports transactions.
143
+ end
144
+
145
+ # Rollback a transaction.
146
+
147
+ def rollback
148
+ # nop, not supported?
149
+ # FIXME: InnoDB supports transactions.
150
+ end
151
+
152
+ private
153
+
154
+ def create_table(klass)
155
+ columns = columns_for_class(klass)
156
+
157
+ sql = "CREATE TABLE #{klass::OGTABLE} (#{columns.join(', ')}"
158
+
159
+ # Create table constrains.
160
+
161
+ if klass.__meta and constrains = klass.__meta[:sql_constrain]
162
+ sql << ", #{constrains.join(', ')}"
163
+ end
164
+
165
+ sql << ");"
166
+
167
+ # Create indices.
168
+
169
+ if klass.__meta and indices = klass.__meta[:index]
170
+ for data in indices
171
+ idx, options = *data
172
+ idx = idx.to_s
173
+ pre_sql, post_sql = options[:pre], options[:post]
174
+ idxname = idx.gsub(/ /, "").gsub(/,/, "_").gsub(/\(.*\)/, "")
175
+ sql << " CREATE #{pre_sql} INDEX #{klass::OGTABLE}_#{idxname}_idx #{post_sql} ON #{klass::OGTABLE} (#{idx});"
176
+ end
177
+ end
178
+
179
+ @conn.query_with_result = false
180
+
181
+ begin
182
+ @conn.query(sql)
183
+ Logger.info "Created table '#{klass::OGTABLE}'."
184
+ rescue => ex
185
+ if ex.errno == 1050 # table already exists.
186
+ Logger.debug 'Table already exists' if $DBG
187
+ return
188
+ else
189
+ raise
190
+ end
191
+ end
192
+
193
+ # Create join tables if needed. Join tables are used in
194
+ # 'many_to_many' relations.
195
+
196
+ if klass.__meta and join_tables = klass.__meta[:join_tables]
197
+ for join_table in join_tables
198
+ begin
199
+ @conn.query("CREATE TABLE #{join_table} (key1 integer NOT NULL, key2 integer NOT NULL)")
200
+ @conn.query("CREATE INDEX #{join_table}_key1_idx ON #{join_table} (key1)")
201
+ @conn.query("CREATE INDEX #{join_table}_key2_idx ON #{join_table} (key2)")
202
+ rescue => ex
203
+ if ex.errno == 1050 # table already exists.
204
+ Logger.debug 'Join table already exists'
205
+ else
206
+ raise
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
212
+
213
+ def create_column_map(klass)
214
+ conn.query_with_result = true
215
+ res = @conn.query "SELECT * FROM #{klass::OGTABLE} LIMIT 1"
216
+ map = {}
217
+
218
+ res.num_fields.times do |i|
219
+ map[res.fetch_field.name.intern] = i
220
+ end
221
+
222
+ return map
223
+ ensure
224
+ res.close if res
225
+ end
226
+
227
+ def write_prop(p)
228
+ if p.klass.ancestors.include?(Integer)
229
+ return "#\{@#{p.symbol} || 'NULL'\}"
230
+ elsif p.klass.ancestors.include?(Float)
231
+ return "#\{@#{p.symbol} || 'NULL'\}"
232
+ elsif p.klass.ancestors.include?(String)
233
+ return %|#\{@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol})\}'" : 'NULL'\}|
234
+ elsif p.klass.ancestors.include?(Time)
235
+ return %|#\{@#{p.symbol} ? "'#\{#{self.class}.timestamp(@#{p.symbol})\}'" : 'NULL'\}|
236
+ elsif p.klass.ancestors.include?(Date)
237
+ return %|#\{@#{p.symbol} ? "'#\{#{self.class}.date(@#{p.symbol})\}'" : 'NULL'\}|
238
+ elsif p.klass.ancestors.include?(TrueClass)
239
+ return "#\{@#{p.symbol} ? \"'1'\" : 'NULL' \}"
240
+ else
241
+ # gmosx: keep the '' for nil symbols.
242
+ return %|#\{@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol}.to_yaml)\}'" : "''"\}|
243
+ end
244
+ end
245
+
246
+ def read_prop(p, col)
247
+ if p.klass.ancestors.include?(Integer)
248
+ return "res[#{col} + offset].to_i"
249
+ elsif p.klass.ancestors.include?(Float)
250
+ return "res[#{col} + offset].to_f"
251
+ elsif p.klass.ancestors.include?(String)
252
+ return "res[#{col} + offset]"
253
+ elsif p.klass.ancestors.include?(Time)
254
+ return "#{self.class}.parse_timestamp(res[#{col} + offset])"
255
+ elsif p.klass.ancestors.include?(Date)
256
+ return "#{self.class}.parse_date(res[#{col} + offset])"
257
+ elsif p.klass.ancestors.include?(TrueClass)
258
+ return "('0' != res[#{col} + offset])"
259
+ else
260
+ return "YAML.load(res[#{col} + offset])"
261
+ end
262
+ end
263
+
264
+ def eval_og_insert(klass)
265
+ props = klass.properties
266
+ values = props.collect { |p| write_prop(p) }.join(',')
267
+
268
+ sql = "INSERT INTO #{klass::OGTABLE} (#{props.collect {|p| p.symbol.to_s}.join(',')}) VALUES (#{values})"
269
+
270
+ klass.class_eval %{
271
+ def og_insert(store)
272
+ #{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
273
+ store.conn.query_with_result = false
274
+ store.conn.query "#{sql}"
275
+ @#{klass.pk_symbol} = store.conn.insert_id
276
+ #{Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
277
+ end
278
+ }
279
+ end
280
+
281
+ end
282
+
283
+ end
@@ -0,0 +1,238 @@
1
+ begin
2
+ require 'postgres'
3
+ rescue Object => ex
4
+ Logger.error 'Ruby-PostgreSQL bindings are not installed!'
5
+ Logger.error ex
6
+ end
7
+
8
+ require 'og/store/sql'
9
+
10
+ # Customize the standard postgres resultset to make
11
+ # more compatible with Og.
12
+
13
+ class PGresult
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
+ end
36
+
37
+ module Og
38
+
39
+ module PsqlUtils
40
+ include SqlUtils
41
+
42
+ def escape(str)
43
+ return nil unless str
44
+ return PGconn.escape(str)
45
+ end
46
+ end
47
+
48
+ # A Store that persists objects into a PostgreSQL database.
49
+ # To read documentation about the methods, consult the documentation
50
+ # for SqlStore and Store.
51
+ #
52
+ # === Design
53
+ #
54
+ # The getvalue interface is used instead of each for extra
55
+ # performance.
56
+
57
+ class PsqlStore < SqlStore
58
+ extend PsqlUtils
59
+ include PsqlUtils
60
+
61
+ def self.create(options)
62
+ # gmosx: system is used to avoid shell expansion.
63
+ system 'createdb', options[:name], '-U', options[:user]
64
+ super
65
+ end
66
+
67
+ def self.destroy(options)
68
+ system 'dropdb', options[:name], '-U', options[:user]
69
+ super
70
+ end
71
+
72
+ def initialize(options)
73
+ super
74
+
75
+ @conn = PGconn.connect(
76
+ options[:address],
77
+ options[:port], nil, nil,
78
+ options[:name],
79
+ options[:user].to_s,
80
+ options[:password].to_s
81
+ )
82
+ rescue => ex
83
+ # gmosx: any idea how to better test this?
84
+ if ex.to_s =~ /database .* does not exist/i
85
+ Logger.info "Database '#{options[:name]}' not found!"
86
+ self.class.create(options)
87
+ retry
88
+ end
89
+ raise
90
+ end
91
+
92
+ def close
93
+ @conn.close
94
+ super
95
+ end
96
+
97
+ def enchant(klass, manager)
98
+ klass.const_set 'OGSEQ', "#{table(klass)}_oid_seq"
99
+ klass.property :oid, Fixnum, :sql => 'serial PRIMARY KEY'
100
+ super
101
+ end
102
+
103
+ def query(sql)
104
+ Logger.debug sql if $DBG
105
+ return @conn.exec(sql)
106
+ rescue => ex
107
+ handle_sql_exception(ex, sql)
108
+ end
109
+
110
+ def exec(sql)
111
+ Logger.debug sql if $DBG
112
+ @conn.exec(sql).clear
113
+ rescue => ex
114
+ handle_sql_exception(ex, sql)
115
+ end
116
+
117
+ private
118
+
119
+ def create_table(klass)
120
+ columns = columns_for_class(klass)
121
+
122
+ sql = "CREATE TABLE #{klass::OGTABLE} (#{columns.join(', ')}"
123
+
124
+ # Create table constrains.
125
+
126
+ if klass.__meta and constrains = klass.__meta[:sql_constrain]
127
+ sql << ", #{constrains.join(', ')}"
128
+ end
129
+
130
+ sql << ") WITHOUT OIDS;"
131
+
132
+ # Create indices.
133
+
134
+ if klass.__meta and indices = klass.__meta[:index]
135
+ for data in indices
136
+ idx, options = *data
137
+ idx = idx.to_s
138
+ pre_sql, post_sql = options[:pre], options[:post]
139
+ idxname = idx.gsub(/ /, "").gsub(/,/, "_").gsub(/\(.*\)/, "")
140
+ sql << " CREATE #{pre_sql} INDEX #{klass::OGTABLE}_#{idxname}_idx #{post_sql} ON #{klass::OGTABLE} (#{idx});"
141
+ end
142
+ end
143
+
144
+ begin
145
+ @conn.exec(sql).clear
146
+ Logger.info "Created table '#{klass::OGTABLE}'."
147
+ rescue Object => ex
148
+ # gmosx: any idea how to better test this?
149
+ if ex.to_s =~ /relation .* already exists/i
150
+ Logger.debug 'Table already exists' if $DBG
151
+ return
152
+ else
153
+ raise
154
+ end
155
+ end
156
+
157
+ # Create join tables if needed. Join tables are used in
158
+ # 'many_to_many' relations.
159
+
160
+ if klass.__meta and join_tables = klass.__meta[:join_tables]
161
+ for join_table in join_tables
162
+ begin
163
+ @conn.exec("CREATE TABLE #{join_table} (key1 integer NOT NULL, key2 integer NOT NULL)").clear
164
+ @conn.exec("CREATE INDEX #{join_table}_key1_idx ON #{join_table} (key1)").clear
165
+ @conn.exec("CREATE INDEX #{join_table}_key2_idx ON #{join_table} (key2)").clear
166
+ rescue Object => ex
167
+ # gmosx: any idea how to better test this?
168
+ if ex.to_s =~ /relation .* already exists/i
169
+ Logger.debug 'Join table already exists'
170
+ else
171
+ raise
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ def drop_table(klass)
179
+ super
180
+ exec "DROP SEQUENCE #{klass::OGSEQ}"
181
+ end
182
+
183
+ def create_column_map(klass)
184
+ res = @conn.exec "SELECT * FROM #{klass::OGTABLE} LIMIT 1"
185
+ map = {}
186
+
187
+ for column in res.fields
188
+ map[column.intern] = res.fieldnum(column)
189
+ end
190
+
191
+ return map
192
+ ensure
193
+ res.clear if res
194
+ end
195
+
196
+ def read_prop(p, col)
197
+ if p.klass.ancestors.include?(Integer)
198
+ return "res.getvalue(row, #{col} + offset).to_i"
199
+ elsif p.klass.ancestors.include?(Float)
200
+ return "res.getvalue(row, #{col} + offset).to_f"
201
+ elsif p.klass.ancestors.include?(String)
202
+ return "res.getvalue(row, #{col} + offset)"
203
+ elsif p.klass.ancestors.include?(Time)
204
+ return "#{self.class}.parse_timestamp(res.getvalue(row, #{col} + offset))"
205
+ elsif p.klass.ancestors.include?(Date)
206
+ return "#{self.class}.parse_date(res.getvalue(row, #{col} + offset))"
207
+ elsif p.klass.ancestors.include?(TrueClass)
208
+ return %|('t' == res.getvalue(row, #{col} + offset))|
209
+ else
210
+ return "YAML.load(res.getvalue(row, #{col} + offset))"
211
+ end
212
+ end
213
+
214
+ #--
215
+ # TODO: create stored procedure.
216
+ #++
217
+
218
+ def eval_og_insert(klass)
219
+ props = klass.properties
220
+ values = props.collect { |p| write_prop(p) }.join(',')
221
+
222
+ sql = "INSERT INTO #{klass::OGTABLE} (#{props.collect {|p| p.symbol.to_s}.join(',')}) VALUES (#{values})"
223
+
224
+ klass.class_eval %{
225
+ def og_insert(store)
226
+ #{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
227
+ res = store.conn.exec "SELECT nextval('#{klass::OGSEQ}')"
228
+ @#{klass.pk_symbol} = res.getvalue(0, 0).to_i
229
+ res.clear
230
+ store.conn.exec("#{sql}").clear
231
+ #{Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
232
+ end
233
+ }
234
+ end
235
+
236
+ end
237
+
238
+ end