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,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
+