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.
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