begin require 'postgres' rescue Object => ex Logger.error 'Ruby-PostgreSQL bindings are not installed!' Logger.error ex end require 'og/store/sql' # Customize the standard postgres resultset to make # more compatible with Og. class PGresult def blank? 0 == num_tuples end def next self end def each_row for row in (0...num_tuples) yield(self, row) end end def first_value val = getvalue(0, 0) clear return val end alias_method :close, :clear end module Og module PsqlUtils include SqlUtils def escape(str) return nil unless str return PGconn.escape(str) end end # A Store that persists objects into a PostgreSQL database. # To read documentation about the methods, consult the documentation # for SqlStore and Store. # # === Design # # The getvalue interface is used instead of each for extra # performance. class PsqlStore < SqlStore extend PsqlUtils include PsqlUtils def self.create(options) # gmosx: system is used to avoid shell expansion. system 'createdb', options[:name], '-U', options[:user] super end def self.destroy(options) system 'dropdb', options[:name], '-U', options[:user] super end def initialize(options) super @conn = PGconn.connect( options[:address], options[:port], nil, nil, options[:name], options[:user].to_s, options[:password].to_s ) schema_order = options[:schema_order] encoding = options[:encoding] min_messages = options[:min_messages] @conn.exec("SET search_path TO #{schema_order}") if schema_order @conn.exec("SET client_encoding TO '#{encoding}'") if encoding @conn.exec("SET client_min_messages TO '#{min_messages}'") if min_messages rescue => ex # gmosx: any idea how to better test this? if ex.to_s =~ /database .* does not exist/i Logger.info "Database '#{options[:name]}' not found!" self.class.create(options) retry end raise end def close @conn.close super end def enchant(klass, manager) klass.const_set 'OGSEQ', "#{table(klass)}_oid_seq" klass.property :oid, Fixnum, :sql => 'serial PRIMARY KEY' super end def query(sql) Logger.debug sql if $DBG return @conn.exec(sql) rescue => ex handle_sql_exception(ex, sql) end def exec(sql) Logger.debug sql if $DBG @conn.exec(sql).clear rescue => ex handle_sql_exception(ex, sql) end private def create_table(klass) fields = fields_for_class(klass) sql = "CREATE TABLE #{klass::OGTABLE} (#{fields.join(', ')}" # Create table constrains. if klass.__meta and constrains = klass.__meta[:sql_constrain] sql << ", #{constrains.join(', ')}" end sql << ") WITHOUT OIDS;" # Create indices. if klass.__meta and indices = klass.__meta[:index] for data in indices idx, options = *data idx = idx.to_s pre_sql, post_sql = options[:pre], options[:post] idxname = idx.gsub(/ /, "").gsub(/,/, "_").gsub(/\(.*\)/, "") sql << " CREATE #{pre_sql} INDEX #{klass::OGTABLE}_#{idxname}_idx #{post_sql} ON #{klass::OGTABLE} (#{idx});" end end begin @conn.exec(sql).clear Logger.info "Created table '#{klass::OGTABLE}'." rescue Object => ex # gmosx: any idea how to better test this? if ex.to_s =~ /relation .* already exists/i Logger.debug 'Table already exists' if $DBG return else raise end end # Create join tables if needed. Join tables are used in # 'many_to_many' relations. if klass.__meta and join_tables = klass.__meta[:join_tables] for join_table in join_tables begin @conn.exec("CREATE TABLE #{join_table} (key1 integer NOT NULL, key2 integer NOT NULL)").clear @conn.exec("CREATE INDEX #{join_table}_key1_idx ON #{join_table} (key1)").clear @conn.exec("CREATE INDEX #{join_table}_key2_idx ON #{join_table} (key2)").clear rescue Object => ex # gmosx: any idea how to better test this? if ex.to_s =~ /relation .* already exists/i Logger.debug 'Join table already exists' else raise end end end end end def drop_table(klass) super exec "DROP SEQUENCE #{klass::OGSEQ}" end def create_field_map(klass) res = @conn.exec "SELECT * FROM #{klass::OGTABLE} LIMIT 1" map = {} for field in res.fields map[field.intern] = res.fieldnum(field) end return map ensure res.clear if res end def read_prop(p, col) if p.klass.ancestors.include?(Integer) return "#{self.class}.parse_int(res.getvalue(row, #{col} + offset))" elsif p.klass.ancestors.include?(Float) return "#{self.class}.parse_float(res.getvalue(row, #{col} + offset))" elsif p.klass.ancestors.include?(String) return "res.getvalue(row, #{col} + offset)" elsif p.klass.ancestors.include?(Time) return "#{self.class}.parse_timestamp(res.getvalue(row, #{col} + offset))" elsif p.klass.ancestors.include?(Date) return "#{self.class}.parse_date(res.getvalue(row, #{col} + offset))" elsif p.klass.ancestors.include?(TrueClass) return %|('t' == res.getvalue(row, #{col} + offset))| else return "YAML.load(res.getvalue(row, #{col} + offset))" end end #-- # TODO: create stored procedure. #++ def eval_og_insert(klass) props = klass.properties values = props.collect { |p| write_prop(p) }.join(',') sql = "INSERT INTO #{klass::OGTABLE} (#{props.collect {|p| p.symbol.to_s}.join(',')}) VALUES (#{values})" klass.class_eval %{ def og_insert(store) #{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)} res = store.conn.exec "SELECT nextval('#{klass::OGSEQ}')" @#{klass.pk_symbol} = res.getvalue(0, 0).to_i res.clear store.conn.exec("#{sql}").clear #{Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)} end } end end end