og 0.5.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.
@@ -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