diff --git a/lib/database_cleaner/active_record/truncation.rb b/lib/database_cleaner/active_record/truncation.rb index 1f3a995..c591d03 100644 --- a/lib/database_cleaner/active_record/truncation.rb +++ b/lib/database_cleaner/active_record/truncation.rb @@ -226,20 +226,27 @@ def database_cleaner_table_cache # but then the table is cleaned. In other words, this function tells us if the given table # was ever inserted into. def has_been_used?(table) - return has_rows?(table) unless has_sequence?(table) + sequence = serial_sequence(table) + return has_rows?(table) unless sequence - cur_val = select_value("SELECT last_value from #{table}_id_seq;").to_i + cur_val = select_value("SELECT last_value from #{sequence};").to_i cur_val > 0 end - def has_sequence?(table) - select_value("SELECT true FROM pg_class WHERE relname = '#{table}_id_seq';") - end - def has_rows?(table) select_value("SELECT true FROM #{table} LIMIT 1;") end + # Looks up the real sequence name instead of guessing "#{table}_id_seq", which can be + # wrong once Postgres truncates it to fit its 63-byte identifier limit. + def serial_sequence(table) + select_value("SELECT pg_get_serial_sequence(#{quote(table)}, 'id')") + rescue ::ActiveRecord::StatementInvalid => e + raise unless e.cause.is_a?(PG::UndefinedColumn) # no "id" column at all + + nil + end + def tables_with_schema rows = select_rows <<-_SQL SELECT schemaname || '.' || tablename diff --git a/spec/database_cleaner/active_record/truncation_spec.rb b/spec/database_cleaner/active_record/truncation_spec.rb index f624243..6b644a9 100644 --- a/spec/database_cleaner/active_record/truncation_spec.rb +++ b/spec/database_cleaner/active_record/truncation_spec.rb @@ -98,6 +98,36 @@ expect(strategy.send(:connection)).to receive(:truncate_tables).with(['users'], {:truncate_option=>:restrict}) strategy.clean end + + if helper.db == :postgres + context "with a table name at Postgres' identifier length limit" do + # :only this table: the rest of this file leaves "users"/"user_profiles" in whatever + # state earlier (mocked) examples left them in, and "user_profiles" has a real FK + # onto "users" that RESTRICT would trip on unrelated to what's under test here. + subject(:strategy) { described_class.new(pre_count: true, only: [long_table_name]) } + let(:long_table_name) { "u" * 58 + "_long" } # 63 chars: Postgres' NAMEDATALEN limit + + before do + connection.execute("CREATE TABLE #{long_table_name} (id SERIAL PRIMARY KEY, name INTEGER);") + end + + after do + connection.execute("DROP TABLE IF EXISTS #{long_table_name};") + end + + it "does not raise when checking whether the table has been used" do + expect { strategy.clean }.not_to raise_error + end + + it "detects activity on the table via its actual (differently-truncated) sequence" do + connection.execute("INSERT INTO #{long_table_name} (name) VALUES (1);") + connection.execute("INSERT INTO #{long_table_name} (name) VALUES (2);") + + expect(strategy.send(:connection)).to receive(:truncate_tables).with([long_table_name], {truncate_option: :restrict}) + strategy.clean + end + end + end end describe "with truncate_option set to cascade" do