lib/og/store/psql.rb in og-0.24.0 vs lib/og/store/psql.rb in og-0.25.0
- old
+ new
@@ -3,10 +3,67 @@
rescue Object => ex
Logger.error 'Ruby-PostgreSQL bindings are not installed!'
Logger.error ex
end
+class PGconn
+ # Lists all the tables within the database.
+
+ def list_tables
+ r = self.exec "SELECT c.relname FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind='r' AND n.nspname NOT IN ('pg_catalog', 'pg_toast') AND pg_catalog.pg_table_is_visible(c.oid)"
+ ret = r.result.flatten
+ r.clear
+ ret
+ end
+
+ # Returns true if a table exists within the database, false
+ # otherwise.
+
+ def table_exists?(table) #rp: this should be abstracted to the sql abstractor
+ r = self.exec "SELECT c.relname FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind='r' AND n.nspname NOT IN ('pg_catalog', 'pg_toast') AND pg_catalog.pg_table_is_visible(c.oid) AND c.relname='#{self.class.escape(table.to_s)}'"
+ ret = r.result.size != 0
+ r.clear
+ ret
+ end
+
+ # Returns the PostgreSQL OID of a table within the database or
+ # nil if it doesn't exist. Mostly for internal usage.
+
+ def table_oid(table)
+ r = self.exec "SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind='r' AND n.nspname NOT IN ('pg_catalog', 'pg_toast') AND pg_catalog.pg_table_is_visible(c.oid) AND c.relname='#{self.class.escape(table.to_s)}'"
+ ret = r.result.flatten.first
+ r.clear
+ ret
+ end
+
+ # Returns an array of arrays containing the list of fields within a
+ # table. Each element contains two elements, the first is the field
+ # name and the second is the field type. Returns nil if the table
+ # does not exist.
+
+ def table_field_list(table)
+ return nil unless pg_oid = table_oid(table)
+ r = self.exec "SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod) FROM pg_catalog.pg_attribute a WHERE a.attrelid = '#{pg_oid}' AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum"
+ ret = r.result
+ r.clear
+ ret
+ end
+
+ # Returns an array of arrays containing the PostgreSQL foreign keys
+ # within a table. The first element is the constraint name and the
+ # second element is the constraint definition.
+
+ def table_foreign_keys(table)
+ return nil unless pg_oid = table_oid(table)
+ r = self.exec "SELECT conname, pg_catalog.pg_get_constraintdef(oid, true) as condef FROM pg_catalog.pg_constraint r WHERE r.conrelid = '#{pg_oid}' AND r.contype = 'f'"
+ ret = r.result
+ r.clear
+ ret
+ end
+
+end
+
require 'og/store/sql'
# Customize the standard postgres resultset to make
# more compatible with Og.
@@ -69,10 +126,12 @@
end
# A Store that persists objects into a PostgreSQL database.
# To read documentation about the methods, consult the documentation
# for SqlStore and Store.
+#
+# This is the reference Og store.
#
# === Design
#
# The getvalue interface is used instead of each for extra
# performance.
@@ -90,10 +149,32 @@
def self.destroy(options)
system 'dropdb', options[:name], '-U', options[:user]
super
end
+ # Purges all tables from the database.
+
+ def self.destroy_tables(options)
+
+ conn = PGconn.connect(
+ options[:address],
+ options[:port], nil, nil,
+ options[:name],
+ options[:user].to_s,
+ options[:password].to_s
+ )
+
+ conn.list_tables.each do |table|
+ sql = "DROP TABLE #{table} CASCADE"
+ conn.exec sql
+ Logger.debug "Dropped database table #{table}"
+ end
+
+ conn.close
+ end
+
+
def initialize(options)
super
@typemap.update(Og::Blob => 'bytea')
@@ -132,11 +213,11 @@
klass.const_set 'OGSEQ', "#{table(klass.schema_inheritance_root_class)}_oid_seq"
else
klass.const_set 'OGSEQ', "#{table(klass)}_oid_seq"
end
- if klass.ann.this.primary_key.symbol == :oid
+ if klass.ann.self.primary_key.symbol == :oid
unless klass.properties.include? :oid
klass.property :oid, Fixnum, :sql => 'serial PRIMARY KEY'
end
end
super
@@ -163,92 +244,179 @@
res.clear
changed
end
# Start a new transaction.
-
+
def start
# neumann: works with earlier PSQL databases too.
exec('BEGIN TRANSACTION') if @transaction_nesting < 1
@transaction_nesting += 1
end
private
- def create_table(klass)
- fields = fields_for_class(klass)
+ # Adds foreign key constraints to a join table, replicating all
+ # modifications to OIDs to the join tables and also purging
+ # any left over data from deleted records (at the time of
+ # implementation, self-join cases left this data here).
- sql = "CREATE TABLE #{klass::OGTABLE} (#{fields.join(', ')}"
-
- # Create table constraints.
-
- if constraints = klass.ann.this[:sql_constraint]
- sql << ", #{constraints.join(', ')}"
+ def create_join_table_foreign_key_constraints(klass,info)
+
+ table_list = @conn.list_tables
+
+ needed_tables = [ info[:table], info[:first_table], info[:second_table] ]
+ missing_tables = Array.new
+ needed_tables.each do |table|
+ missing_tables << table unless table_list.include?(table)
end
-
- sql << ") WITHOUT OIDS;"
-
- # Create indices.
-
- if indices = klass.ann.this[: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});"
+ if missing_tables.size > 0
+ msg = "Join table #{info[:table]} needs PostgreSQL foreign key constraints but the following table"
+ msg << (missing_tables.size > 1 ? "s were " : " was ")
+ msg << "missing: "
+ missing_tables.each do |table|
+ msg << "#{table}, "
end
+ Logger.debug msg[0..-3] + ". (Should be retried later)."
+ return false
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'
- return
- else
- raise
+ #This info should maybe be in join metadata?
+ target_class = nil
+ klass.relations.each do |rel|
+ if rel.join_table == info[:table]
+ target_class = rel.target_class
+ break
end
end
+ if target_class
+ Logger.debug "Adding PostgreSQL foreign key constraints to #{info[:table]}"
+
+ # TODO: This should also interrograte the constraint definition
+ # incase people meddle with the database in an insane fashion
+ # (very, very low priority)
+
+ existing_constraints = @conn.table_foreign_keys(info[:table]).map {|fk| fk.first}
+
+ constraints = Array.new
+ constraints << { :table => info[:first_table], :join_table => info[:table], :primary_key => klass.primary_key.field || klass.primary_key.symbol, :foreign_key => info[:first_key] }
+ constraints << { :table => info[:second_table], :join_table => info[:table], :primary_key => target_class.primary_key.field || target_class.primary_key.symbol, :foreign_key => info[:second_key] }
+
+ constraints.each do |constraint|
+ constraint_name = "ogc_#{constraint[:table]}_#{constraint[:foreign_key]}"
+ if existing_constraints.include?(constraint_name)
+ Logger.debug "PostgreSQL foreign key constraint linking #{constraint[:foreign_key]} on #{constraint[:join_table]} to #{constraint[:primary_key]} on #{constraint[:table]} already exists (#{constraint_name})."
+ next
+ end
+ sql = "ALTER TABLE #{constraint[:join_table]} ADD CONSTRAINT #{constraint_name} FOREIGN KEY (#{constraint[:foreign_key]}) REFERENCES #{constraint[:table]} (#{constraint[:primary_key]}) ON UPDATE CASCADE ON DELETE CASCADE"
+ @conn.exec(sql).clear
+ Logger.debug "Added PostgreSQL foreign key constraint linking #{constraint[:foreign_key]} on #{constraint[:join_table]} to #{constraint[:primary_key]} on #{constraint[:table]} (#{constraint_name})."
+ end
+ end
+ end
+
+ def create_table(klass)
+ fields = fields_for_class(klass)
+
+ unless @conn.table_exists? klass::OGTABLE
+
+ sql = "CREATE TABLE #{klass::OGTABLE} (#{fields.join(', ')}"
+
+ # Create table constraints.
+
+ if constraints = klass.ann.self[:sql_constraint]
+ sql << ", #{constraints.join(', ')}"
+ end
+
+ sql << ") WITHOUT OIDS;"
+
+ # Create indices.
+
+ if indices = klass.ann.self[: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
+
+ @conn.exec(sql).clear
+ Logger.info "Created table '#{klass::OGTABLE}'."
+ else
+ Logger.debug "Table #{klass::OGTABLE} already exists"
+ #rp: basic field interrogation
+ # TODO: Add type checking.
+
+ actual_fields = @conn.table_field_list(klass::OGTABLE).map {|pair| pair.first}
+
+ #Make new ones always - don't destroy by default because it might contain data you want back.
+ need_fields = fields.each do |needed_field|
+ field_name = needed_field[0..(needed_field.index(' ')-1)]
+ next if actual_fields.include?(field_name)
+
+ if @options[:evolve_schema] == true
+ Logger.debug "Adding field '#{needed_field}' to '#{klass::OGTABLE}'"
+ sql = "ALTER TABLE #{klass::OGTABLE} ADD COLUMN #{needed_field}"
+ @conn.exec(sql)
+ else
+ Logger.info "WARNING: Table '#{klass::OGTABLE}' is missing field '#{needed_field}' and :evolve_schema is not set to true!"
+ end
+ end
+
+ #Drop old ones
+ needed_fields = fields.map {|f| f =~ /^([^ ]+)/; $1}
+ actual_fields.each do |obsolete_field|
+ next if needed_fields.include?(obsolete_field)
+ if @options[:evolve_schema] == true and @options[:evolve_schema_cautious] == false
+ sql = "ALTER TABLE #{klass::OGTABLE} DROP COLUMN #{obsolete_field}"
+ Logger.debug "Removing obsolete field '#{obsolete_field}' from '#{klass::OGTABLE}'"
+ @conn.exec(sql)
+ else
+ Logger.info "WARNING: You have an obsolete field '#{obsolete_field}' on table '#{klass::OGTABLE}' and :evolve_schema is not set or is in cautious mode!"
+ end
+ end
+ end
+
# Create join tables if needed. Join tables are used in
# 'many_to_many' relations.
-
- if join_tables = klass.ann.this[:join_tables]
- for info in join_tables
- begin
- create_join_table_sql(info).each do |sql|
- @conn.exec(sql).clear
- end
- Logger.debug "Created jointable '#{info[:table]}'."
- 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' if $DBG
+ players = klass.resolve_remote_relations.map{|rel| rel.owner_class if rel.collection}.compact.uniq
+ players << klass
+ players.each do |player|
+ if join_tables = player.ann.self[:join_tables]
+ for info in join_tables
+ unless @conn.table_exists? info[:table]
+ create_join_table_sql(info).each do |sql|
+ @conn.exec(sql).clear
+ end
+ Logger.debug "Created jointable '#{info[:table]}'."
else
- raise
+ Logger.debug "Join table '#{info[:table]}' already exists."
end
+ create_join_table_foreign_key_constraints(player,info)
end
end
end
+
end
def drop_table(klass)
- super
- exec "DROP SEQUENCE #{klass::OGSEQ}"
+ # foreign key constraints will remove the need to do manual cleanup on
+ # postgresql join tables.
+ exec "DROP TABLE #{klass.table} CASCADE"
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
@@ -282,27 +450,27 @@
if klass.schema_inheritance?
props << Property.new(:symbol => :ogtype, :klass => String)
values << ", '#{klass}'"
end
-
+
sql = "INSERT INTO #{klass::OGTABLE} (#{props.collect {|p| field_for_property(p)}.join(',')}) VALUES (#{values})"
klass.class_eval %{
def og_insert(store)
- #{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
+ #{Glue::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)}
+ #{Glue::Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
end
}
end
def eval_og_allocate(klass)
- if klass.ann.this[:subclasses]
+ if klass.ann.self[:subclasses]
klass.module_eval %{
def self.og_allocate(res, row = 0)
Object.constant(res.getvalue(row, 0)).allocate
end
}
@@ -326,5 +494,6 @@
end
# * George Moschovitis <gm@navel.gr>
# * Michael Neumann <mneumann@ntecs.de>
# * Ysabel <deb@ysabel.org>
+# * Rob Pitt <rob@motionpath.com>
\ No newline at end of file