mirror of
https://github.com/github/rails.git
synced 2026-02-03 10:45:01 -05:00
r4325@asus: jeremy | 2005-11-12 03:57:46 -0800
PostgreSQL: correctly discover custom primary key sequences. PostgreSQL: smarter sequence name defaults, stricter last_insert_id, warn on pk without sequence. Base.reset_sequence_name analogous to reset_table_name (mostly useful for testing). Base.define_attr_method allows nil values. References #2594. git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@2985 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
*SVN*
|
||||
|
||||
* Base.reset_sequence_name analogous to reset_table_name (mostly useful for testing). Base.define_attr_method allows nil values. [Jeremy Kemper]
|
||||
|
||||
* PostgreSQL: smarter sequence name defaults, stricter last_insert_id, warn on pk without sequence. [Jeremy Kemper]
|
||||
|
||||
* PostgreSQL: correctly discover custom primary key sequences. #2594 [Blair Zajac <blair@orcaware.com>, meadow.nnick@gmail.com, Jeremy Kemper]
|
||||
|
||||
* SQLServer: don't report limits for unsupported field types. #2835 [Ryan Tomayko]
|
||||
|
||||
* Include the Enumerable module in ActiveRecord::Errors. [Rick Bradley <rick@rickbradley.com>]
|
||||
|
||||
@@ -646,9 +646,16 @@ module ActiveRecord #:nodoc:
|
||||
"type"
|
||||
end
|
||||
|
||||
# Default sequence_name. Use set_sequence_name to override.
|
||||
# Lazy-set the sequence name to the connection's default. This method
|
||||
# is only ever called once since set_sequence_name overrides it.
|
||||
def sequence_name
|
||||
connection.default_sequence_name(table_name, primary_key)
|
||||
reset_sequence_name
|
||||
end
|
||||
|
||||
def reset_sequence_name
|
||||
default = connection.default_sequence_name(table_name, primary_key)
|
||||
set_sequence_name(default)
|
||||
default
|
||||
end
|
||||
|
||||
# Sets the table name to use to the given value, or (if the value
|
||||
@@ -1053,12 +1060,12 @@ module ActiveRecord #:nodoc:
|
||||
def define_attr_method(name, value=nil, &block)
|
||||
sing = class << self; self; end
|
||||
sing.send :alias_method, "original_#{name}", name
|
||||
if value
|
||||
if block_given?
|
||||
sing.send :define_method, name, &block
|
||||
else
|
||||
# use eval instead of a block to work around a memory leak in dev
|
||||
# mode in fcgi
|
||||
sing.class_eval "def #{name}; #{value.to_s.inspect}; end"
|
||||
else
|
||||
sing.send :define_method, name, &block
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ module ActiveRecord
|
||||
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
|
||||
execute(sql, name)
|
||||
table = sql.split(" ", 4)[2]
|
||||
id_value || last_insert_id(table, sequence_name)
|
||||
id_value || last_insert_id(table, sequence_name || default_sequence_name(table, pk))
|
||||
end
|
||||
|
||||
def query(sql, name = nil) #:nodoc:
|
||||
@@ -199,23 +199,34 @@ module ActiveRecord
|
||||
@schema_search_path ||= query('SHOW search_path')[0][0]
|
||||
end
|
||||
|
||||
def default_sequence_name(table_name, pk = 'id')
|
||||
"#{table_name}_#{pk}_seq"
|
||||
def default_sequence_name(table_name, pk = nil)
|
||||
default_pk, default_seq = pk_and_sequence_for(table_name)
|
||||
default_seq || "#{table_name}_#{pk || default_pk}_seq"
|
||||
end
|
||||
|
||||
# Set the sequence to the max value of the table's pk.
|
||||
def reset_pk_sequence!(table)
|
||||
pk, sequence = pk_and_sequence_for(table)
|
||||
if pk and sequence
|
||||
select_value <<-end_sql, 'Reset sequence'
|
||||
SELECT setval('#{sequence}', (SELECT COALESCE(MAX(#{pk})+(SELECT increment_by FROM #{sequence}), (SELECT min_value FROM #{sequence})) FROM #{table}), false)
|
||||
end_sql
|
||||
# Resets sequence to the max value of the table's pk if present.
|
||||
def reset_pk_sequence!(table, pk = nil, sequence = nil)
|
||||
unless pk and sequence
|
||||
default_pk, default_sequence = pk_and_sequence_for(table)
|
||||
pk ||= default_pk
|
||||
sequence ||= default_sequence
|
||||
end
|
||||
if pk
|
||||
if sequence
|
||||
select_value <<-end_sql, 'Reset sequence'
|
||||
SELECT setval('#{sequence}', (SELECT COALESCE(MAX(#{pk})+(SELECT increment_by FROM #{sequence}), (SELECT min_value FROM #{sequence})) FROM #{table}), false)
|
||||
end_sql
|
||||
else
|
||||
@logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Find a table's primary key and sequence.
|
||||
def pk_and_sequence_for(table)
|
||||
execute(<<-end_sql, 'Find pk sequence')[0]
|
||||
# First try looking for a sequence with a dependency on the
|
||||
# given table's primary key.
|
||||
result = execute(<<-end_sql, 'PK and serial sequence')[0]
|
||||
SELECT attr.attname, (name.nspname || '.' || seq.relname)
|
||||
FROM pg_class seq,
|
||||
pg_attribute attr,
|
||||
@@ -232,11 +243,29 @@ module ActiveRecord
|
||||
AND cons.contype = 'p'
|
||||
AND dep.refobjid = '#{table}'::regclass
|
||||
end_sql
|
||||
|
||||
if result.nil? or result.empty?
|
||||
# If that fails, try parsing the primary key's default value.
|
||||
# Support the 7.x and 8.0 nextval('foo'::text) as well as
|
||||
# the 8.1+ nextval('foo'::regclass).
|
||||
# TODO: assumes sequence is in same schema as table.
|
||||
result = execute(<<-end_sql, 'PK and custom sequence')[0]
|
||||
SELECT attr.attname, (name.nspname || '.' || split_part(def.adsrc, '\\\'', 2))
|
||||
FROM pg_class t
|
||||
JOIN pg_namespace name ON (t.relnamespace = name.oid)
|
||||
JOIN pg_attribute attr ON (t.oid = attrelid)
|
||||
JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
|
||||
JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
|
||||
WHERE t.oid = '#{table}'::regclass
|
||||
AND cons.contype = 'p'
|
||||
AND def.adsrc ~ 'nextval\\\\(\\\'[^\\\']*\\\'::[^\\\\)]*\\\\)'
|
||||
end_sql
|
||||
end
|
||||
result
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
|
||||
def rename_table(name, new_name)
|
||||
execute "ALTER TABLE #{name} RENAME TO #{new_name}"
|
||||
end
|
||||
@@ -281,9 +310,7 @@ module ActiveRecord
|
||||
BYTEA_COLUMN_TYPE_OID = 17
|
||||
|
||||
def last_insert_id(table, sequence_name)
|
||||
if sequence_name
|
||||
@connection.exec("SELECT currval('#{sequence_name}')")[0][0].to_i
|
||||
end
|
||||
Integer(@connection.exec("SELECT currval('#{sequence_name}')")[0][0])
|
||||
end
|
||||
|
||||
def select(sql, name = nil)
|
||||
|
||||
@@ -839,10 +839,10 @@ class BasicsTest < Test::Unit::TestCase
|
||||
def test_column_name_properly_quoted
|
||||
col_record = ColumnName.new
|
||||
col_record.references = 40
|
||||
col_record.save
|
||||
assert col_record.save
|
||||
col_record.references = 41
|
||||
col_record.save
|
||||
c2 = ColumnName.find(col_record.id)
|
||||
assert col_record.save
|
||||
assert_not_nil c2 = ColumnName.find(col_record.id)
|
||||
assert_equal(41, c2.references)
|
||||
end
|
||||
|
||||
|
||||
@@ -173,27 +173,45 @@ end
|
||||
if Account.connection.respond_to?(:reset_pk_sequence!)
|
||||
class FixturesResetPkSequenceTest < Test::Unit::TestCase
|
||||
fixtures :accounts
|
||||
fixtures :companies
|
||||
|
||||
def test_resets_to_min_pk
|
||||
Account.delete_all
|
||||
Account.connection.reset_pk_sequence!(Account.table_name)
|
||||
def setup
|
||||
@instances = [Account.new(:credit_limit => 50), Company.new(:name => 'RoR Consulting')]
|
||||
end
|
||||
|
||||
one = Account.new(:credit_limit => 50)
|
||||
one.save!
|
||||
assert_equal 1, one.id
|
||||
def test_resets_to_min_pk_with_specified_pk_and_sequence
|
||||
@instances.each do |instance|
|
||||
model = instance.class
|
||||
model.delete_all
|
||||
model.connection.reset_pk_sequence!(model.table_name, model.primary_key, model.sequence_name)
|
||||
|
||||
instance.save!
|
||||
assert_equal 1, instance.id, "Sequence reset for #{model.table_name} failed."
|
||||
end
|
||||
end
|
||||
|
||||
def test_resets_to_min_pk_with_default_pk_and_sequence
|
||||
@instances.each do |instance|
|
||||
model = instance.class
|
||||
model.delete_all
|
||||
model.connection.reset_pk_sequence!(model.table_name)
|
||||
|
||||
instance.save!
|
||||
assert_equal 1, instance.id, "Sequence reset for #{model.table_name} failed."
|
||||
end
|
||||
end
|
||||
|
||||
def test_create_fixtures_resets_sequences
|
||||
# create_fixtures performs reset_pk_sequence!
|
||||
max_id = create_fixtures('accounts').inject(0) do |max_id, (name, fixture)|
|
||||
fixture_id = fixture['id'].to_i
|
||||
fixture_id > max_id ? fixture_id : max_id
|
||||
end
|
||||
@instances.each do |instance|
|
||||
max_id = create_fixtures(instance.class.table_name).inject(0) do |max_id, (name, fixture)|
|
||||
fixture_id = fixture['id'].to_i
|
||||
fixture_id > max_id ? fixture_id : max_id
|
||||
end
|
||||
|
||||
# Clone the last fixture to check that it gets the next greatest id.
|
||||
another = Account.new(:credit_limit => 1200)
|
||||
another.save!
|
||||
assert_equal max_id + 1, another.id
|
||||
# Clone the last fixture to check that it gets the next greatest id.
|
||||
instance.save!
|
||||
assert_equal max_id + 1, instance.id, "Sequence reset for #{instance.class.table_name} failed."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -362,6 +362,9 @@ if ActiveRecord::Base.connection.supports_migrations?
|
||||
ActiveRecord::Base.table_name_suffix = ""
|
||||
Reminder.reset_table_name
|
||||
assert_equal "schema_info", ActiveRecord::Migrator.schema_info_table_name
|
||||
ensure
|
||||
ActiveRecord::Base.table_name_prefix = ""
|
||||
ActiveRecord::Base.table_name_suffix = ""
|
||||
end
|
||||
|
||||
def test_proper_table_name
|
||||
@@ -398,17 +401,20 @@ if ActiveRecord::Base.connection.supports_migrations?
|
||||
ActiveRecord::Base.table_name_prefix = 'prefix_'
|
||||
ActiveRecord::Base.table_name_suffix = '_suffix'
|
||||
Reminder.reset_table_name
|
||||
Reminder.reset_sequence_name
|
||||
WeNeedReminders.up
|
||||
assert Reminder.create("content" => "hello world", "remind_at" => Time.now)
|
||||
assert_equal "hello world", Reminder.find(:first).content
|
||||
|
||||
WeNeedReminders.down
|
||||
assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
|
||||
ensure
|
||||
ActiveRecord::Base.table_name_prefix = ''
|
||||
ActiveRecord::Base.table_name_suffix = ''
|
||||
Reminder.reset_table_name
|
||||
Reminder.reset_sequence_name
|
||||
end
|
||||
|
||||
|
||||
def test_migrator_with_duplicates
|
||||
assert_raises(ActiveRecord::DuplicateMigrationVersionError) do
|
||||
ActiveRecord::Migrator.migrate(File.dirname(__FILE__) + '/fixtures/migrations_with_duplicate/', nil)
|
||||
|
||||
Reference in New Issue
Block a user