og 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,359 @@
1
+ # code:
2
+ # * George Moschovitis <gm@navel.gr>
3
+ #
4
+ # (c) 2004 Navel, all rights reserved.
5
+ # $Id: psql.rb 159 2004-11-18 10:18:30Z gmosx $
6
+
7
+ require "postgres"
8
+
9
+ require "og/backend"
10
+
11
+ module Og
12
+
13
+ # = Utils
14
+ #
15
+ # A collection of useful utilities.
16
+ #
17
+ module Utils
18
+
19
+ # Escape an SQL string
20
+ #
21
+ def self.escape(str)
22
+ return nil unless str
23
+ return PGconn.escape(str)
24
+ end
25
+
26
+ # Convert a ruby time to an sql timestamp.
27
+ # TODO: Optimize this
28
+ #
29
+ def self.timestamp(time = Time.now)
30
+ return nil unless time
31
+ return time.strftime("%Y-%m-%d %H:%M:%S")
32
+ end
33
+
34
+ # Output YYY-mm-dd
35
+ # TODO: Optimize this
36
+ #
37
+ def self.date(date)
38
+ return nil unless date
39
+ return "#{date.year}-#{date.month}-#{date.mday}"
40
+ end
41
+
42
+ # Parse sql datetime
43
+ # TODO: Optimize this
44
+ #
45
+ def self.parse_timestamp(str)
46
+ return Time.parse(str)
47
+ end
48
+
49
+ # Input YYYY-mm-dd
50
+ # TODO: Optimize this
51
+ #
52
+ def self.parse_date(str)
53
+ return nil unless str
54
+ return Date.strptime(str)
55
+ end
56
+
57
+ # Return an sql string evaluator for the property.
58
+ # No need to optimize this, used only to precalculate code.
59
+ # YAML is used to store general Ruby objects to be more
60
+ # portable.
61
+ #
62
+ # FIXME: add extra handling for float.
63
+ #
64
+ def self.write_prop(p)
65
+ if p.klass.ancestors.include?(Integer)
66
+ return "#\{@#{p.symbol} || 'NULL'\}"
67
+ elsif p.klass.ancestors.include?(Float)
68
+ return "#\{@#{p.symbol} || 'NULL'\}"
69
+ elsif p.klass.ancestors.include?(String)
70
+ return "'#\{Og::Utils.escape(@#{p.symbol})\}'"
71
+ elsif p.klass.ancestors.include?(Time)
72
+ return %|#\{@#{p.symbol} ? "'#\{Og::Utils.timestamp(@#{p.symbol})\}'" : 'NULL'\}|
73
+ elsif p.klass.ancestors.include?(Date)
74
+ return %|#\{@#{p.symbol} ? "'#\{Og::Utils.date(@#{p.symbol})\}'" : 'NULL'\}|
75
+ elsif p.klass.ancestors.include?(TrueClass)
76
+ return "#\{@#{p.symbol} || 'NULL'\}"
77
+ else
78
+ return %|#\{@#{p.symbol} ? "'#\{Og::Utils.escape(@#{p.symbol}.to_yaml)\}'" : "''"\}|
79
+ end
80
+ end
81
+
82
+ # Return an evaluator for reading the property.
83
+ # No need to optimize this, used only to precalculate code.
84
+ #
85
+ def self.read_prop(p, idx)
86
+ if p.klass.ancestors.include?(Integer)
87
+ return "res.getvalue(tuple, #{idx}).to_i()"
88
+ elsif p.klass.ancestors.include?(Float)
89
+ return "res.getvalue(tuple, #{idx}).to_f()"
90
+ elsif p.klass.ancestors.include?(String)
91
+ return "res.getvalue(tuple, #{idx})"
92
+ elsif p.klass.ancestors.include?(Time)
93
+ return "Og::Utils.parse_timestamp(res.getvalue(tuple, #{idx}))"
94
+ elsif p.klass.ancestors.include?(Date)
95
+ return "Og::Utils.parse_date(res.getvalue(tuple, #{idx}))"
96
+ elsif p.klass.ancestors.include?(TrueClass)
97
+ return "('true' == res.getvalue(tuple, #{idx}))"
98
+ else
99
+ return "YAML::load(res.getvalue(tuple, #{idx}))"
100
+ end
101
+ end
102
+
103
+ # Returns the code that actually inserts the object into the
104
+ # database. Returns the code as String.
105
+ #
106
+ def self.insert_code(klass, sql, pre_cb, post_cb)
107
+ %{
108
+ #{pre_cb}
109
+ res = conn.db.query("SELECT nextval('#{klass::DBSEQ}')")
110
+ @oid = res.getvalue(0, 0).to_i
111
+ conn.exec "#{sql}"
112
+ #{post_cb}
113
+ }
114
+ end
115
+
116
+ # generate the mapping of the database fields to the
117
+ # object properties.
118
+ #
119
+ def self.calc_field_index(klass, og)
120
+ res = og.query "SELECT * FROM #{klass::DBTABLE} LIMIT 1"
121
+ meta = og.managed_classes[klass]
122
+
123
+ for field in res.fields
124
+ meta.field_index[field] = res.fieldnum(field)
125
+ end
126
+ end
127
+
128
+ # Generate the property for oid
129
+ #
130
+ def self.eval_og_oid(klass)
131
+ klass.class_eval %{
132
+ prop_accessor :oid, Fixnum, :sql => "integer PRIMARY KEY"
133
+ }
134
+ end
135
+ end
136
+
137
+ # = PsqlBackend
138
+ #
139
+ # Implements a PostgreSQL powered backend.
140
+ #
141
+ class PsqlBackend < Og::Backend
142
+
143
+ # A mapping between Ruby and SQL types.
144
+ #
145
+ TYPEMAP = {
146
+ Integer => "integer",
147
+ Fixnum => "integer",
148
+ Float => "float",
149
+ String => "text",
150
+ Time => "timestamp",
151
+ Date => "date",
152
+ TrueClass => "boolean",
153
+ Object => "text",
154
+ Array => "text",
155
+ Hash => "text"
156
+ }
157
+
158
+ # Intitialize the connection to the RDBMS.
159
+ #
160
+ def initialize(config)
161
+ begin
162
+ @conn = PGconn.connect(nil, nil, nil, nil, config[:database],
163
+ config[:user], config[:password])
164
+ rescue => ex
165
+ # gmosx: any idea how to better test this?
166
+ if ex.to_s =~ /database .* does not exist/i
167
+ $log.info "Database '#{config[:database]}' not found!"
168
+ PsqlBackend.create_db(config[:database], config[:user])
169
+ retry
170
+ end
171
+ raise
172
+ end
173
+ end
174
+
175
+ # Create the database.
176
+ #
177
+ def self.create_db(database, user = nil, password = nil)
178
+ $log.info "Creating database '#{database}'."
179
+ `createdb #{database} -U #{user}`
180
+ end
181
+
182
+ # Drop the database.
183
+ #
184
+ def self.drop_db(database, user = nil, password = nil)
185
+ $log.info "Dropping database '#{database}'."
186
+ `dropdb #{database} -U #{user}`
187
+ end
188
+
189
+ # Execute an SQL query and return the result
190
+ #
191
+ def query(sql)
192
+ $log.debug sql if $DBG
193
+ return @conn.exec(sql)
194
+ end
195
+
196
+ # Execute an SQL query, no result returned.
197
+ #
198
+ def exec(sql)
199
+ $log.debug sql if $DBG
200
+ res = @conn.exec(sql)
201
+ res.clear()
202
+ end
203
+
204
+ # Execute an SQL query and return the result. Wrapped in a rescue
205
+ # block.
206
+ #
207
+ def safe_query(sql)
208
+ $log.debug sql if $DBG
209
+ begin
210
+ return @conn.exec(sql)
211
+ rescue => ex
212
+ $log.error "DB error #{ex}, [#{sql}]"
213
+ $log.error ex.backtrace
214
+ return nil
215
+ end
216
+ end
217
+
218
+ # Execute an SQL query, no result returned. Wrapped in a rescue
219
+ # block.
220
+ #
221
+ def safe_exec(sql)
222
+ $log.debug sql if $DBG
223
+ begin
224
+ res = @conn.exec(sql)
225
+ res.clear()
226
+ rescue => ex
227
+ $log.error "DB error #{ex}, [#{sql}]"
228
+ $log.error ex.backtrace
229
+ end
230
+ end
231
+
232
+ # Check if it is a valid resultset.
233
+ #
234
+ def valid?(res)
235
+ return !(res.nil? or 0 == res.num_tuples)
236
+ end
237
+
238
+ # Create the managed object table. The properties of the
239
+ # object are mapped to the table columns. Additional sql relations
240
+ # and constrains are created (indicices, sequences, etc).
241
+ #
242
+ def create_table(klass)
243
+ fields = []
244
+
245
+ klass.__props.each do |p|
246
+ klass.sql_index(p.symbol) if p.meta[:sql_index]
247
+
248
+ field = "#{p.symbol}"
249
+
250
+ if p.meta and p.meta[:sql]
251
+ field << " #{p.meta[:sql]}"
252
+ else
253
+ field << " #{TYPEMAP[p.klass]}"
254
+ end
255
+
256
+ fields << field
257
+ end
258
+
259
+ sql = "CREATE TABLE #{klass::DBTABLE} (#{fields.join(', ')}"
260
+
261
+ # Create table constrains
262
+
263
+ if klass.__meta and constrains = klass.__meta[:sql_constrain]
264
+ sql << ", #{constrains.join(', ')}"
265
+ end
266
+
267
+ sql << ") WITHOUT OIDS;"
268
+
269
+ # Create indices
270
+
271
+ if klass.__meta
272
+ for data in klass.__meta[:sql_index]
273
+ idx, options = *data
274
+ idx = idx.to_s
275
+ pre_sql, post_sql = options[:pre], options[:post]
276
+ idxname = idx.gsub(/ /, "").gsub(/,/, "_").gsub(/\(.*\)/, "")
277
+ sql << " CREATE #{pre_sql} INDEX #{klass::DBTABLE}_#{idxname}_idx #{post_sql} ON #{klass::DBTABLE} (#{idx});"
278
+ end
279
+ end
280
+
281
+ begin
282
+ exec(sql)
283
+ $log.info "Created table '#{klass::DBTABLE}'."
284
+ rescue => ex
285
+ # gmosx: any idea how to better test this?
286
+ if ex.to_s =~ /relation .* already exists/i
287
+ $log.debug "Table already exists" if $DBG
288
+ else
289
+ raise
290
+ end
291
+ end
292
+
293
+ # create the sequence for this table. Even if the table
294
+ # uses the oids_seq, attempt to create it. This makes
295
+ # the system more fault tolerant.
296
+ begin
297
+ exec "CREATE SEQUENCE #{klass::DBSEQ}"
298
+ $log.info "Created sequence '#{klass::DBSEQ}'."
299
+ rescue => ex
300
+ # gmosx: any idea how to better test this?
301
+ if ex.to_s =~ /relation .* already exists/i
302
+ $log.debug "Sequence already exists" if $DBG
303
+ else
304
+ raise
305
+ end
306
+ end
307
+ end
308
+
309
+ # Drop the managed object table
310
+ #
311
+ def drop_table(klass)
312
+ super
313
+ exec "DROP SEQUENCE #{klass::DBSEQ}"
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
+ entity = klass.new()
323
+ entity.og_deserialize(res, 0)
324
+
325
+ # get_join_fields(res, 0, entity, join_fields) if join_fields
326
+
327
+ res.clear()
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_tuples)
339
+ entity = klass.new()
340
+ entity.og_deserialize(res, tuple)
341
+
342
+ # get_join_fields(res, tuple, entity, join_fields) if join_fields
343
+
344
+ entities << entity
345
+ end
346
+
347
+ res.clear()
348
+ return entities
349
+ end
350
+
351
+ # Return a single integer value from the resultset.
352
+ #
353
+ def get_int(res, idx = 0)
354
+ return res.getvalue(0, idx).to_i
355
+ end
356
+
357
+ end
358
+
359
+ end # module
@@ -0,0 +1,265 @@
1
+ # code:
2
+ # * George Moschovitis <gm@navel.gr>
3
+ #
4
+ # (c) 2004 Navel, all rights reserved.
5
+ # $Id: connection.rb 167 2004-11-23 14:03:10Z gmosx $
6
+
7
+ module Og;
8
+
9
+ require "glue/property"
10
+ require "glue/array"
11
+ require "glue/time"
12
+
13
+ # = Connection
14
+ #
15
+ # A Connection to the Database. This file defines the skeleton
16
+ # functionality. A backend specific implementation file (driver)
17
+ # implements all methods.
18
+ #
19
+ # === Future
20
+ #
21
+ # - support caching.
22
+ # - support prepared statements.
23
+ #
24
+ class Connection
25
+ # The frontend (Og) contains useful strucutres.
26
+ attr_reader :og
27
+
28
+ # The backend
29
+ attr_reader :db
30
+
31
+ # If set to true, the select methods deserialize the
32
+ # resultset to create entities.
33
+ attr_accessor :deserialize
34
+
35
+ # Initialize a connection to the database
36
+ #
37
+ def initialize(og)
38
+ @og = og
39
+ @db = @og.config[:backend].new(@og.config)
40
+ @deserialize = true
41
+ $log.debug "Created DB connection."
42
+ end
43
+
44
+ # Close the connection to the database
45
+ #
46
+ def close()
47
+ @db.close()
48
+ $log.debug "Closed DB connection."
49
+ end
50
+
51
+ # Save an object to the database. Insert if this is a new object or
52
+ # update if this is already stored in the database.
53
+ #
54
+ def save(obj)
55
+ if obj.oid
56
+ # object allready inserted, update!
57
+ obj.og_update(self)
58
+ else
59
+ # not in the database, insert!
60
+ obj.og_insert(self)
61
+ end
62
+ end
63
+ alias_method :<<, :save
64
+ alias_method :put, :save
65
+
66
+ # Force insertion of managed object.
67
+ #
68
+ def insert(obj)
69
+ obj.og_insert(self)
70
+ end
71
+
72
+ # Force update of managed object.
73
+ #
74
+ def update(obj)
75
+ obj.og_update(self)
76
+ end
77
+
78
+ # Update only specific fields of the managed object.
79
+ #
80
+ # Input:
81
+ # sql = the sql code to updated the properties.
82
+ #
83
+ # WARNING: the object in memoryis not updated.
84
+ #--
85
+ # TODO: should update the object in memory.
86
+ #++
87
+ #
88
+ def update_properties(update_sql, obj_or_oid, klass = nil)
89
+ oid = obj_or_oid.to_i
90
+ klass = obj_or_oid.class unless klass
91
+
92
+ exec "UPDATE #{klass::DBTABLE} SET #{update_sql} WHERE oid=#{oid}"
93
+ end
94
+ alias_method :pupdate, :update_properties
95
+
96
+ # Load an object from the database.
97
+ #
98
+ # Input:
99
+ # oid = the object oid, OR the object name.
100
+ #
101
+ def load(oid, klass)
102
+ if oid.to_i > 0 # a valid Fixnum ?
103
+ load_by_oid(oid, klass)
104
+ else
105
+ load_by_name(oid, klass)
106
+ end
107
+ end
108
+ alias_method :get, :load
109
+
110
+ # Load an object by oid.
111
+ #
112
+ def load_by_oid(oid, klass)
113
+ res = query "SELECT * FROM #{klass::DBTABLE} WHERE oid=#{oid}"
114
+ @deserialize? @db.deserialize_one(res, klass) : res
115
+ end
116
+ alias_method :get_by_oid, :load_by_oid
117
+
118
+ # Load an object by name.
119
+ #
120
+ def load_by_name(name, klass)
121
+ res = query "SELECT * FROM #{klass::DBTABLE} WHERE name='#{name}'"
122
+ @deserialize? @db.deserialize_one(res, klass) : res
123
+ end
124
+ alias_method :get_by_name, :load_by_name
125
+
126
+ # Load all objects of the given klass.
127
+ # Used to be called 'collect' in an earlier version.
128
+ #
129
+ def load_all(klass, extrasql = nil)
130
+ res = query "SELECT * FROM #{klass::DBTABLE} #{extrasql}"
131
+ @deserialize? @db.deserialize_all(res, klass) : res
132
+ end
133
+ alias_method :get_all, :load_all
134
+
135
+ # Perform a standard SQL query to the database. Deserializes the
136
+ # results.
137
+ #
138
+ def select(sql, klass)
139
+ unless sql =~ /SELECT/i
140
+ sql = "SELECT * FROM #{klass::DBTABLE} WHERE #{sql}"
141
+ end
142
+
143
+ res = @db.safe_query(sql)
144
+ @deserialize? @db.deserialize_all(res, klass) : res
145
+ end
146
+
147
+ # Optimized for one result.
148
+ #
149
+ def select_one(sql, klass)
150
+ unless sql =~ /SELECT/i
151
+ sql = "SELECT * FROM #{klass::DBTABLE} WHERE #{sql}"
152
+ end
153
+
154
+ res = @db.safe_query(sql)
155
+ @deserialize? @db.deserialize_one(res, klass) : res
156
+ end
157
+
158
+ # Perform a count query.
159
+ #
160
+ def count(sql, klass = nil)
161
+ unless sql =~ /SELECT/i
162
+ sql = "SELECT COUNT(*) FROM #{klass::DBTABLE} WHERE #{sql}"
163
+ end
164
+
165
+ res = @db.safe_query(sql)
166
+
167
+ return @db.get_int(res)
168
+ end
169
+
170
+ # Delete an object from the database. Allways perform a deep delete.
171
+ #
172
+ # No need to optimize here with pregenerated code. Deletes are
173
+ # not used as much as reads or writes.
174
+ #
175
+ # === Input:
176
+ #
177
+ # obj_or_oid = Object or oid to delete.
178
+ # klass = Class of object (can be nil if an object is passed)
179
+ #
180
+ def delete(obj_or_oid, klass = nil, cascade = true)
181
+ oid = obj_or_oid.to_i
182
+ klass = obj_or_oid.class unless klass
183
+
184
+ # this is a class callback!
185
+ if klass.respond_to?(:og_pre_delete)
186
+ klass.og_pre_delete(self, oid)
187
+ end
188
+
189
+ # TODO: implement this as stored procedure? naaah.
190
+ transaction do |tx|
191
+ tx.exec "DELETE FROM #{klass::DBTABLE} WHERE oid=#{oid}"
192
+
193
+ if cascade and klass.respond_to?(:og_descendants)
194
+ klass.og_descendants.each do |dclass, linkback|
195
+ tx.exec "DELETE FROM #{dclass::DBTABLE} WHERE #{linkback}=#{oid}"
196
+ end
197
+ end
198
+ end
199
+ end
200
+ alias_method :delete!, :delete
201
+
202
+ # Create the managed object table. The properties of the
203
+ # object are mapped to the table columns. Additional sql relations
204
+ # and constrains are created (indicices, sequences, etc).
205
+ #
206
+ def create_table(klass)
207
+ @db.create_table(klass)
208
+ end
209
+
210
+ # Drop the managed object table.
211
+ #
212
+ def drop_table(klass)
213
+ @db.drop_table(klass)
214
+ end
215
+
216
+ # Execute an SQL query and return the result
217
+ #
218
+ def query(sql)
219
+ @db.safe_query(sql)
220
+ end
221
+
222
+ # Execute an SQL query, no result returned.
223
+ #
224
+ def exec(sql)
225
+ @db.safe_exec(sql)
226
+ end
227
+
228
+ # Start a new transaction.
229
+ #
230
+ def start
231
+ @db.start()
232
+ end
233
+
234
+ # Commit a transaction.
235
+ #
236
+ def commit
237
+ @db.commit()
238
+ end
239
+
240
+ # Rollback transaction.
241
+ #
242
+ def rollback
243
+ @db.rollback()
244
+ end
245
+
246
+ # Transaction helper. In the transaction block use
247
+ # the db pointer to the backend.
248
+ #
249
+ def transaction(&block)
250
+ begin
251
+ @db.start()
252
+ yield(@db)
253
+ @db.commit()
254
+ rescue => ex
255
+ $log.error "DB Error: ERROR IN TRANSACTION"
256
+ $log.error #{ex}
257
+ $log.error #{ex.backtrace}
258
+ @db.rollback()
259
+ end
260
+ end
261
+
262
+ end
263
+
264
+ end # module
265
+