og 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,258 @@
1
+ # code:
2
+ # * George Moschovitis <gm@navel.gr>
3
+ #
4
+ # (c) 2004 Navel, all rights reserved.
5
+ # $Id: backend.rb 155 2004-11-13 20:32:12Z gmosx $
6
+
7
+ require "yaml"
8
+
9
+ require "og/connection"
10
+
11
+ module Og
12
+
13
+ # = OgUtils
14
+ #
15
+ # A collection of useful utilities.
16
+ #
17
+ module Utils
18
+
19
+ # The name of the SQL table where objects of this class are stored.
20
+ # The Module separators are replaced with _ and NOT stripped out so
21
+ # that we can convert back to the original notation if needed.
22
+ # The leading module if available is removed.
23
+ #
24
+ def self.table(klass)
25
+ return "_#{klass.name.gsub(/^.*::/, "")}".gsub(/::/, "_").downcase
26
+ end
27
+
28
+ # Returns the props that will be included in the insert query.
29
+ # For some backends the oid should be stripped.
30
+ #
31
+ def self.props_for_insert(klass)
32
+ klass.__props
33
+ end
34
+
35
+ # Precompile the insert code for the given class.
36
+ # The generated code sets the oid when inserting!
37
+ #
38
+ def self.eval_og_insert(klass)
39
+ props = props_for_insert(klass)
40
+
41
+ values = props.collect { |p| write_prop(p) }
42
+
43
+ sql = "INSERT INTO #{table(klass)} (#{props.collect {|p| p.name}.join(',')}) VALUES (#{values.join(',')})"
44
+
45
+ if klass.instance_methods.include?("og_pre_insert")
46
+ pre_cb = "og_pre_insert(conn);"
47
+ else
48
+ pre_cb = ""
49
+ end
50
+
51
+ if klass.instance_methods.include?("og_post_insert")
52
+ post_cb = "og_post_insert(conn);"
53
+ else
54
+ post_cb = ""
55
+ end
56
+
57
+ if klass.instance_methods.include?("og_pre_insert_update")
58
+ pre_cb << "og_pre_insert_update(conn);"
59
+ end
60
+
61
+ if klass.instance_methods.include?("og_post_insert_update")
62
+ post_cb << "og_post_insert_update(conn);"
63
+ end
64
+
65
+ klass.class_eval %{
66
+ def og_insert(conn)
67
+ #{insert_code(klass, sql, pre_cb, post_cb)}
68
+ end
69
+ }
70
+ end
71
+
72
+ # Precompile the update code for the given class.
73
+ # Ignore the oid when updating!
74
+ #
75
+ def self.eval_og_update(klass)
76
+ props = klass.__props.reject { |p| :oid == p.symbol }
77
+
78
+ updates = props.collect { |p|
79
+ "#{p.name}=#{write_prop(p)}"
80
+ }
81
+
82
+ sql = "UPDATE #{klass::DBTABLE} SET #{updates.join(', ')} WHERE oid=#\{@oid\}"
83
+
84
+ if klass.instance_methods.include?("og_pre_update")
85
+ pre_cb = "og_pre_update(conn);"
86
+ else
87
+ pre_cb = ""
88
+ end
89
+
90
+ if klass.instance_methods.include?("og_post_update")
91
+ post_cb = "og_post_update(conn);"
92
+ else
93
+ post_cb = ""
94
+ end
95
+
96
+ if klass.instance_methods.include?("og_pre_insert_update")
97
+ pre_cb << "og_pre_insert_update(conn);"
98
+ end
99
+
100
+ if klass.instance_methods.include?("og_post_insert_update")
101
+ post_cb << "og_post_insert_update(conn);"
102
+ end
103
+
104
+ klass.class_eval %{
105
+ def og_update(conn)
106
+ #{pre_cb}
107
+ conn.exec "#{sql}"
108
+ #{post_cb}
109
+ end
110
+ }
111
+ end
112
+
113
+ # Precompile the code to read objects of the given class
114
+ # from the backend. In order to allow for changing field/attribute
115
+ # orders we have to use a field mapping hash.
116
+ #
117
+ def self.eval_og_deserialize(klass, og)
118
+ calc_field_index(klass, og)
119
+
120
+ props = klass.__props
121
+ code = []
122
+
123
+ props.each do |p|
124
+ if idx = og.managed_classes[klass].field_index[p.name]
125
+ # more fault tolerant if a new field is added and it
126
+ # doesnt exist in the database.
127
+ code << "@#{p.name} = #{Og::Utils.read_prop(p, idx)}"
128
+ end
129
+ end
130
+
131
+ klass.class_eval %{
132
+ def og_deserialize(res, tuple = nil)
133
+ #{code.join('; ')}
134
+ end
135
+ }
136
+ end
137
+
138
+ end
139
+
140
+ # = Backend
141
+ #
142
+ # Abstract backend. A backend communicates with the RDBMS.
143
+ # This is the base class for the various backend implementations.
144
+ #
145
+ class Backend
146
+
147
+ # The actual connection to the database
148
+ attr_accessor :conn
149
+
150
+ # Intitialize the connection to the RDBMS.
151
+ #
152
+ def initialize(config)
153
+ raise "Not implemented"
154
+ end
155
+
156
+ # Close the connection to the RDBMS.
157
+ #
158
+ def close()
159
+ @conn.close()
160
+ end
161
+
162
+ # Create the database.
163
+ #
164
+ def self.create_db(database, user = nil, password = nil)
165
+ $log.info "Creating database '#{database}'."
166
+ end
167
+
168
+ # Drop the database.
169
+ #
170
+ def self.drop_db(database, user = nil, password = nil)
171
+ $log.info "Dropping database '#{database}'."
172
+ end
173
+
174
+ # Execute an SQL query and return the result.
175
+ #
176
+ def query(sql)
177
+ raise "Not implemented"
178
+ end
179
+
180
+ # Execute an SQL query, no result returned.
181
+ #
182
+ def exec(sql)
183
+ raise "Not implemented"
184
+ end
185
+
186
+ # Execute an SQL query and return the result. Wrapped in a rescue
187
+ # block.
188
+ #
189
+ def safe_query(sql)
190
+ raise "Not implemented"
191
+ end
192
+
193
+ # Execute an SQL query, no result returned. Wrapped in a rescue
194
+ # block.
195
+ #
196
+ def safe_exec(sql)
197
+ raise "Not implemented"
198
+ end
199
+
200
+ # Check if it is a valid resultset.
201
+ #
202
+ def valid?(res)
203
+ raise "Not implemented"
204
+ end
205
+
206
+ # Start a new transaction.
207
+ #
208
+ def start
209
+ exec "START TRANSACTION"
210
+ end
211
+
212
+ # Commit a transaction.
213
+ #
214
+ def commit
215
+ exec "COMMIT"
216
+ end
217
+
218
+ # Rollback transaction.
219
+ #
220
+ def rollback
221
+ exec "ROLLBACK"
222
+ end
223
+
224
+ # Create the managed object table. The properties of the
225
+ # object are mapped to the table columns. Additional sql relations
226
+ # and constrains are created (indicices, sequences, etc).
227
+ #
228
+ def create_table(klass)
229
+ return if query("SELECT * FROM #{klass::DBTABLE} LIMIT 1")
230
+ end
231
+
232
+ # Drop the managed object table
233
+ #
234
+ def drop_table(klass)
235
+ exec "DROP TABLE #{klass::DBTABLE}"
236
+ end
237
+
238
+ # Deserialize one row of the resultset.
239
+ #
240
+ def deserialize_one(res, klass)
241
+ raise "Not implemented"
242
+ end
243
+
244
+ # Deserialize all rows of the resultset.
245
+ #
246
+ def deserialize_all(res, klass)
247
+ raise "Not implemented"
248
+ end
249
+
250
+ # Return a single integer value from the resultset.
251
+ #
252
+ def get_int(res, idx = 0)
253
+ raise "Not implemented"
254
+ end
255
+
256
+ end
257
+
258
+ end # module
@@ -0,0 +1,360 @@
1
+ # code:
2
+ # * George Moschovitis <gm@navel.gr>
3
+ # * Elias Athanasopoulos <elathan@navel.gr>
4
+ #
5
+ # (c) 2004 Navel, all rights reserved.
6
+ # $Id: mysql.rb 159 2004-11-18 10:18:30Z gmosx $
7
+
8
+ require "mysql"
9
+
10
+ require "og/backend"
11
+
12
+ module Og
13
+
14
+ # = Utils
15
+ #
16
+ # A collection of useful utilities.
17
+ #
18
+ module Utils
19
+
20
+ # Escape an SQL string
21
+ #
22
+ def self.escape(str)
23
+ return nil unless str
24
+ return Mysql.quote(str)
25
+ end
26
+
27
+ # Convert a ruby time to an sql timestamp.
28
+ # TODO: Optimize this
29
+ #
30
+ def self.timestamp(time = Time.now)
31
+ return nil unless time
32
+ return time.strftime("%Y%m%d%H%M%S")
33
+ end
34
+
35
+ # Output YYY-mm-dd
36
+ # TODO: Optimize this
37
+ #
38
+ def self.date(date)
39
+ return nil unless date
40
+ return "#{date.year}-#{date.month}-#{date.mday}"
41
+ end
42
+
43
+ # Parse sql datetime
44
+ # TODO: Optimize this
45
+ #
46
+ def self.parse_timestamp(str)
47
+ return Time.parse(str)
48
+ end
49
+
50
+ # Input YYYY-mm-dd
51
+ # TODO: Optimize this
52
+ #
53
+ def self.parse_date(str)
54
+ return nil unless str
55
+ return Date.strptime(str)
56
+ end
57
+
58
+ # Return an sql string evaluator for the property.
59
+ # No need to optimize this, used only to precalculate code.
60
+ # YAML is used to store general Ruby objects to be more
61
+ # portable.
62
+ #
63
+ # FIXME: add extra handling for float.
64
+ #
65
+ def self.write_prop(p)
66
+ if p.klass.ancestors.include?(Integer)
67
+ return "#\{@#{p.symbol} || 'NULL'\}"
68
+ elsif p.klass.ancestors.include?(Float)
69
+ return "#\{@#{p.symbol} || 'NULL'\}"
70
+ elsif p.klass.ancestors.include?(String)
71
+ return "'#\{Og::Utils.escape(@#{p.symbol})\}'"
72
+ elsif p.klass.ancestors.include?(Time)
73
+ return %|#\{@#{p.symbol} ? "'#\{Og::Utils.timestamp(@#{p.symbol})\}'" : 'NULL'\}|
74
+ elsif p.klass.ancestors.include?(Date)
75
+ return %|#\{@#{p.symbol} ? "'#\{Og::Utils.date(@#{p.symbol})\}'" : 'NULL'\}|
76
+ elsif p.klass.ancestors.include?(TrueClass)
77
+ return "#\{@#{p.symbol} || 'NULL'\}"
78
+ else
79
+ return %|#\{@#{p.symbol} ? "'#\{Og::Utils.escape(@#{p.symbol}.to_yaml)\}'" : "''"\}|
80
+ end
81
+ end
82
+
83
+ # Return an evaluator for reading the property.
84
+ # No need to optimize this, used only to precalculate code.
85
+ #
86
+ def self.read_prop(p, idx)
87
+ if p.klass.ancestors.include?(Integer)
88
+ return "res[#{idx}].to_i()"
89
+ elsif p.klass.ancestors.include?(Float)
90
+ return "res[#{idx}].to_f()"
91
+ elsif p.klass.ancestors.include?(String)
92
+ return "res[#{idx}]"
93
+ elsif p.klass.ancestors.include?(Time)
94
+ return "Og::Utils.parse_timestamp(res[#{idx}])"
95
+ elsif p.klass.ancestors.include?(Date)
96
+ return "Og::Utils.parse_date(res[#{idx}])"
97
+ elsif p.klass.ancestors.include?(TrueClass)
98
+ return "('true' == res[#{idx}])"
99
+ else
100
+ return "YAML::load(res[#{idx}])"
101
+ end
102
+ end
103
+
104
+ # Returns the props that will be included in the insert query.
105
+ # The oid property is rejected because it is mapped to an
106
+ # AUTO_INCREMENT column.
107
+ #
108
+ def self.props_for_insert(klass)
109
+ klass.__props.reject { |p| :oid == p.symbol }
110
+ end
111
+
112
+ # Returns the code that actually inserts the object into the
113
+ # database. Returns the code as String.
114
+ #
115
+ def self.insert_code(klass, sql, pre_cb, post_cb)
116
+ %{
117
+ #{pre_cb}
118
+ conn.exec "#{sql}"
119
+ @oid = conn.db.conn.insert_id()
120
+ #{post_cb}
121
+ }
122
+ end
123
+
124
+ # generate the mapping of the database fields to the
125
+ # object properties.
126
+ #
127
+ def self.calc_field_index(klass, og)
128
+ res = og.query "SELECT * FROM #{klass::DBTABLE} LIMIT 1"
129
+ meta = og.managed_classes[klass]
130
+
131
+ for idx in (0...res.num_fields)
132
+ meta.field_index[res.fetch_field.name] = idx
133
+ end
134
+ end
135
+
136
+ # Generate the property for oid
137
+ #
138
+ def self.eval_og_oid(klass)
139
+ klass.class_eval %{
140
+ prop_accessor :oid, Fixnum, :sql => "integer AUTO_INCREMENT PRIMARY KEY"
141
+ }
142
+ end
143
+ end
144
+
145
+ # = MysqlBackend
146
+ #
147
+ # Implements a MySQL powered backend.
148
+ #
149
+ class MysqlBackend < Og::Backend
150
+
151
+ # A mapping between Ruby and SQL types.
152
+ #
153
+ TYPEMAP = {
154
+ Integer => "integer",
155
+ Fixnum => "integer",
156
+ Float => "float",
157
+ String => "text",
158
+ Time => "timestamp",
159
+ Date => "date",
160
+ TrueClass => "boolean",
161
+ Object => "text",
162
+ Array => "text",
163
+ Hash => "text"
164
+ }
165
+
166
+ # Intitialize the connection to the RDBMS.
167
+ #
168
+ def initialize(config)
169
+ begin
170
+ @conn = Mysql.connect(config[:address], config[:user],
171
+ config[:password], config[:database])
172
+ rescue => ex
173
+ if ex.errno == 1049 # database does not exist.
174
+ $log.info "Database '#{config[:database]}' not found!"
175
+ MysqlBackend.create_db(config[:database], config[:user], config[:password])
176
+ retry
177
+ end
178
+ raise
179
+ end
180
+ end
181
+
182
+ # Create the database.
183
+ #
184
+ def self.create_db(database, user = nil, password = nil)
185
+ $log.info "Creating database '#{database}'."
186
+ `mysqladmin -f --user=#{user} --password=#{password} create #{database}`
187
+ end
188
+
189
+ # Drop the database.
190
+ #
191
+ def self.drop_db(database, user = nil, password = nil)
192
+ $log.info "Dropping database '#{database}'."
193
+ `mysqladmin -f --user=#{user} --password=#{password} drop #{database}`
194
+ end
195
+
196
+ # Execute an SQL query and return the result
197
+ #
198
+ def query(sql)
199
+ $log.debug sql if $DBG
200
+ return @conn.query(sql)
201
+ end
202
+
203
+ # Execute an SQL query, no result returned.
204
+ #
205
+ def exec(sql)
206
+ $log.debug sql if $DBG
207
+ @conn.query(sql)
208
+ end
209
+
210
+ # Execute an SQL query and return the result. Wrapped in a rescue
211
+ # block.
212
+ #
213
+ def safe_query(sql)
214
+ $log.debug sql if $DBG
215
+ begin
216
+ return @conn.query(sql)
217
+ rescue => ex
218
+ $log.error "DB error #{ex}, [#{sql}]"
219
+ $log.error ex.backtrace
220
+ return nil
221
+ end
222
+ end
223
+
224
+ # Execute an SQL query, no result returned. Wrapped in a rescue
225
+ # block.
226
+ #
227
+ def safe_exec(sql)
228
+ $log.debug sql if $DBG
229
+ begin
230
+ @conn.query(sql)
231
+ rescue => ex
232
+ $log.error "DB error #{ex}, [#{sql}]"
233
+ $log.error ex.backtrace
234
+ end
235
+ end
236
+
237
+ # Check if it is a valid resultset.
238
+ #
239
+ def valid?(res)
240
+ return !(res.nil? or 0 == res.num_rows)
241
+ end
242
+
243
+ # Start a new transaction.
244
+ #
245
+ def start
246
+ # no transaction support
247
+ end
248
+
249
+ # Commit a transaction.
250
+ #
251
+ def commit
252
+ # no transaction support
253
+ end
254
+
255
+ # Rollback transaction.
256
+ #
257
+ def rollback
258
+ # no transaction support
259
+ end
260
+
261
+ # Create the managed object table. The properties of the
262
+ # object are mapped to the table columns. Additional sql relations
263
+ # and constrains are created (indicices, sequences, etc).
264
+ #
265
+ def create_table(klass)
266
+ fields = []
267
+
268
+ klass.__props.each do |p|
269
+ klass.sql_index(p.symbol) if p.meta[:sql_index]
270
+
271
+ field = "#{p.symbol}"
272
+
273
+ if p.meta and p.meta[:sql]
274
+ field << " #{p.meta[:sql]}"
275
+ else
276
+ field << " #{TYPEMAP[p.klass]}"
277
+ end
278
+
279
+ fields << field
280
+ end
281
+
282
+ sql = "CREATE TABLE #{klass::DBTABLE} (#{fields.join(', ')}"
283
+
284
+ # Create table constrains
285
+
286
+ if klass.__meta and constrains = klass.__meta[:sql_constrain]
287
+ sql << ", #{constrains.join(', ')}"
288
+ end
289
+
290
+ sql << ');'
291
+
292
+ begin
293
+ exec(sql)
294
+ $log.info "Created table '#{klass::DBTABLE}'."
295
+ rescue => ex
296
+ if ex.errno == 1050 # table already exists.
297
+ $log.debug "Table already exists" if $DBG
298
+ else
299
+ raise
300
+ end
301
+ end
302
+
303
+ # Create indices
304
+
305
+ if klass.__meta
306
+ for data in klass.__meta[:sql_index]
307
+ idx, options = *data
308
+ idx = idx.to_s
309
+ pre_sql, post_sql = options[:pre], options[:post]
310
+ idxname = idx.gsub(/ /, "").gsub(/,/, "_").gsub(/\(.*\)/, "")
311
+ exec "CREATE #{pre_sql} INDEX #{klass::DBTABLE}_#{idxname}_idx #{post_sql} ON #{klass::DBTABLE} (#{idx})"
312
+ end
313
+ end
314
+ end
315
+
316
+ # Deserialize one row of the resultset.
317
+ #
318
+ def deserialize_one(res, klass)
319
+ return nil unless valid?(res)
320
+
321
+ # gmosx: Managed objects should have no params constructor.
322
+ row = res.fetch_row()
323
+ entity = klass.new()
324
+ entity.og_deserialize(row)
325
+
326
+ # get_join_fields(res, 0, entity, join_fields) if join_fields
327
+
328
+ return entity
329
+ end
330
+
331
+ # Deserialize all rows of the resultset.
332
+ #
333
+ def deserialize_all(res, klass)
334
+ return nil unless valid?(res)
335
+
336
+ entities = []
337
+
338
+ for tuple in (0...res.num_rows)
339
+ row = res.fetch_row()
340
+
341
+ entity = klass.new()
342
+ entity.og_deserialize(row)
343
+
344
+ # get_join_fields(res, tuple, entity, join_fields) if join_fields
345
+
346
+ entities << entity
347
+ end
348
+
349
+ return entities
350
+ end
351
+
352
+ # Return a single integer value from the resultset.
353
+ #
354
+ def get_int(res, idx = 0)
355
+ return res.fetch_row[idx].to_i
356
+ end
357
+
358
+ end
359
+
360
+ end # module