require "cases/helper" require "models/default" require "support/schema_dumping_helper" module PGSchemaHelper def with_schema_search_path(schema_search_path) @connection.schema_search_path = schema_search_path @connection.schema_cache.clear! yield if block_given? ensure @connection.schema_search_path = "'$user', public" @connection.schema_cache.clear! end end class SchemaTest < ActiveRecord::PostgreSQLTestCase include PGSchemaHelper self.use_transactional_tests = false SCHEMA_NAME = "test_schema" SCHEMA2_NAME = "test_schema2" TABLE_NAME = "things" CAPITALIZED_TABLE_NAME = "Things" INDEX_A_NAME = "a_index_things_on_name" INDEX_B_NAME = "b_index_things_on_different_columns_in_each_schema" INDEX_C_NAME = "c_index_full_text_search" INDEX_D_NAME = "d_index_things_on_description_desc" INDEX_E_NAME = "e_index_things_on_name_vector" INDEX_A_COLUMN = "name" INDEX_B_COLUMN_S1 = "email" INDEX_B_COLUMN_S2 = "moment" INDEX_C_COLUMN = "(to_tsvector('english', coalesce(things.name, '')))" INDEX_D_COLUMN = "description" INDEX_E_COLUMN = "name_vector" COLUMNS = [ "id integer", "name character varying(50)", "email character varying(50)", "description character varying(100)", "name_vector tsvector", "moment timestamp without time zone default now()" ] PK_TABLE_NAME = "table_with_pk" UNMATCHED_SEQUENCE_NAME = "unmatched_primary_key_default_value_seq" UNMATCHED_PK_TABLE_NAME = "table_with_unmatched_sequence_for_pk" class Thing1 < ActiveRecord::Base self.table_name = "test_schema.things" end class Thing2 < ActiveRecord::Base self.table_name = "test_schema2.things" end class Thing3 < ActiveRecord::Base self.table_name = 'test_schema."things.table"' end class Thing4 < ActiveRecord::Base self.table_name = 'test_schema."Things"' end class Thing5 < ActiveRecord::Base self.table_name = "things" end class Song < ActiveRecord::Base self.table_name = "music.songs" has_and_belongs_to_many :albums end class Album < ActiveRecord::Base self.table_name = "music.albums" has_and_belongs_to_many :songs end def setup @connection = ActiveRecord::Base.connection @connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})" @connection.execute "CREATE TABLE #{SCHEMA_NAME}.\"#{TABLE_NAME}.table\" (#{COLUMNS.join(',')})" @connection.execute "CREATE TABLE #{SCHEMA_NAME}.\"#{CAPITALIZED_TABLE_NAME}\" (#{COLUMNS.join(',')})" @connection.execute "CREATE SCHEMA #{SCHEMA2_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})" @connection.execute "CREATE INDEX #{INDEX_A_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING btree (#{INDEX_A_COLUMN});" @connection.execute "CREATE INDEX #{INDEX_A_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING btree (#{INDEX_A_COLUMN});" @connection.execute "CREATE INDEX #{INDEX_B_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING btree (#{INDEX_B_COLUMN_S1});" @connection.execute "CREATE INDEX #{INDEX_B_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING btree (#{INDEX_B_COLUMN_S2});" @connection.execute "CREATE INDEX #{INDEX_C_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING gin (#{INDEX_C_COLUMN});" @connection.execute "CREATE INDEX #{INDEX_C_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING gin (#{INDEX_C_COLUMN});" @connection.execute "CREATE INDEX #{INDEX_D_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING btree (#{INDEX_D_COLUMN} DESC);" @connection.execute "CREATE INDEX #{INDEX_D_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING btree (#{INDEX_D_COLUMN} DESC);" @connection.execute "CREATE INDEX #{INDEX_E_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING gin (#{INDEX_E_COLUMN});" @connection.execute "CREATE INDEX #{INDEX_E_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING gin (#{INDEX_E_COLUMN});" @connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{PK_TABLE_NAME} (id serial primary key)" @connection.execute "CREATE SEQUENCE #{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}" @connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME} (id integer NOT NULL DEFAULT nextval('#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}'::regclass), CONSTRAINT unmatched_pkey PRIMARY KEY (id))" end teardown do @connection.drop_schema SCHEMA2_NAME, if_exists: true @connection.drop_schema SCHEMA_NAME, if_exists: true end def test_schema_names assert_equal ["public", "test_schema", "test_schema2"], @connection.schema_names end def test_create_schema begin @connection.create_schema "test_schema3" assert @connection.schema_names.include? "test_schema3" ensure @connection.drop_schema "test_schema3" end end def test_raise_create_schema_with_existing_schema begin @connection.create_schema "test_schema3" assert_raises(ActiveRecord::StatementInvalid) do @connection.create_schema "test_schema3" end ensure @connection.drop_schema "test_schema3" end end def test_drop_schema begin @connection.create_schema "test_schema3" ensure @connection.drop_schema "test_schema3" end assert_not_includes @connection.schema_names, "test_schema3" end def test_drop_schema_if_exists @connection.create_schema "some_schema" assert_includes @connection.schema_names, "some_schema" @connection.drop_schema "some_schema", if_exists: true assert_not_includes @connection.schema_names, "some_schema" end def test_habtm_table_name_with_schema ActiveRecord::Base.connection.drop_schema "music", if_exists: true ActiveRecord::Base.connection.create_schema "music" ActiveRecord::Base.connection.execute <<-SQL CREATE TABLE music.albums (id serial primary key); CREATE TABLE music.songs (id serial primary key); CREATE TABLE music.albums_songs (album_id integer, song_id integer); SQL song = Song.create Album.create assert_equal song, Song.includes(:albums).references(:albums).first ensure ActiveRecord::Base.connection.drop_schema "music", if_exists: true end def test_drop_schema_with_nonexisting_schema assert_raises(ActiveRecord::StatementInvalid) do @connection.drop_schema "idontexist" end assert_nothing_raised do @connection.drop_schema "idontexist", if_exists: true end end def test_raise_wrapped_exception_on_bad_prepare assert_raises(ActiveRecord::StatementInvalid) do @connection.exec_query "select * from developers where id = ?", "sql", [bind_param(1)] end end if ActiveRecord::Base.connection.prepared_statements def test_schema_change_with_prepared_stmt altered = false @connection.exec_query "select * from developers where id = $1", "sql", [bind_param(1)] @connection.exec_query "alter table developers add column zomg int", "sql", [] altered = true @connection.exec_query "select * from developers where id = $1", "sql", [bind_param(1)] ensure # We are not using DROP COLUMN IF EXISTS because that syntax is only # supported by pg 9.X @connection.exec_query("alter table developers drop column zomg", "sql", []) if altered end end def test_data_source_exists? [Thing1, Thing2, Thing3, Thing4].each do |klass| name = klass.table_name assert @connection.data_source_exists?(name), "'#{name}' data_source should exist" end end def test_data_source_exists_when_on_schema_search_path with_schema_search_path(SCHEMA_NAME) do assert(@connection.data_source_exists?(TABLE_NAME), "data_source should exist and be found") end end def test_data_source_exists_when_not_on_schema_search_path with_schema_search_path("PUBLIC") do assert(!@connection.data_source_exists?(TABLE_NAME), "data_source exists but should not be found") end end def test_data_source_exists_wrong_schema assert(!@connection.data_source_exists?("foo.things"), "data_source should not exist") end def test_data_source_exists_quoted_names [ %("#{SCHEMA_NAME}"."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}")].each do |given| assert(@connection.data_source_exists?(given), "data_source should exist when specified as #{given}") end with_schema_search_path(SCHEMA_NAME) do given = %("#{TABLE_NAME}") assert(@connection.data_source_exists?(given), "data_source should exist when specified as #{given}") end end def test_data_source_exists_quoted_table with_schema_search_path(SCHEMA_NAME) do assert(@connection.data_source_exists?('"things.table"'), "data_source should exist") end end def test_with_schema_prefixed_table_name assert_nothing_raised do assert_equal COLUMNS, columns("#{SCHEMA_NAME}.#{TABLE_NAME}") end end def test_with_schema_prefixed_capitalized_table_name assert_nothing_raised do assert_equal COLUMNS, columns("#{SCHEMA_NAME}.#{CAPITALIZED_TABLE_NAME}") end end def test_with_schema_search_path assert_nothing_raised do with_schema_search_path(SCHEMA_NAME) do assert_equal COLUMNS, columns(TABLE_NAME) end end end def test_proper_encoding_of_table_name assert_equal '"table_name"', @connection.quote_table_name("table_name") assert_equal '"table.name"', @connection.quote_table_name('"table.name"') assert_equal '"schema_name"."table_name"', @connection.quote_table_name("schema_name.table_name") assert_equal '"schema_name"."table.name"', @connection.quote_table_name('schema_name."table.name"') assert_equal '"schema.name"."table_name"', @connection.quote_table_name('"schema.name".table_name') assert_equal '"schema.name"."table.name"', @connection.quote_table_name('"schema.name"."table.name"') end def test_classes_with_qualified_schema_name assert_equal 0, Thing1.count assert_equal 0, Thing2.count assert_equal 0, Thing3.count assert_equal 0, Thing4.count Thing1.create(id: 1, name: "thing1", email: "thing1@localhost", moment: Time.now) assert_equal 1, Thing1.count assert_equal 0, Thing2.count assert_equal 0, Thing3.count assert_equal 0, Thing4.count Thing2.create(id: 1, name: "thing1", email: "thing1@localhost", moment: Time.now) assert_equal 1, Thing1.count assert_equal 1, Thing2.count assert_equal 0, Thing3.count assert_equal 0, Thing4.count Thing3.create(id: 1, name: "thing1", email: "thing1@localhost", moment: Time.now) assert_equal 1, Thing1.count assert_equal 1, Thing2.count assert_equal 1, Thing3.count assert_equal 0, Thing4.count Thing4.create(id: 1, name: "thing1", email: "thing1@localhost", moment: Time.now) assert_equal 1, Thing1.count assert_equal 1, Thing2.count assert_equal 1, Thing3.count assert_equal 1, Thing4.count end def test_raise_on_unquoted_schema_name assert_raises(ActiveRecord::StatementInvalid) do with_schema_search_path "$user,public" end end def test_without_schema_search_path assert_raises(ActiveRecord::StatementInvalid) { columns(TABLE_NAME) } end def test_ignore_nil_schema_search_path assert_nothing_raised { with_schema_search_path nil } end def test_index_name_exists with_schema_search_path(SCHEMA_NAME) do assert @connection.index_name_exists?(TABLE_NAME, INDEX_A_NAME, true) assert @connection.index_name_exists?(TABLE_NAME, INDEX_B_NAME, true) assert @connection.index_name_exists?(TABLE_NAME, INDEX_C_NAME, true) assert @connection.index_name_exists?(TABLE_NAME, INDEX_D_NAME, true) assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME, true) assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME, true) assert_not @connection.index_name_exists?(TABLE_NAME, "missing_index", true) end end def test_dump_indexes_for_schema_one do_dump_index_tests_for_schema(SCHEMA_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN, INDEX_E_COLUMN) end def test_dump_indexes_for_schema_two do_dump_index_tests_for_schema(SCHEMA2_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S2, INDEX_D_COLUMN, INDEX_E_COLUMN) end def test_dump_indexes_for_schema_multiple_schemas_in_search_path do_dump_index_tests_for_schema("public, #{SCHEMA_NAME}", INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN, INDEX_E_COLUMN) end def test_dump_indexes_for_table_with_scheme_specified_in_name indexes = @connection.indexes("#{SCHEMA_NAME}.#{TABLE_NAME}") assert_equal 5, indexes.size end def test_with_uppercase_index_name @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" with_schema_search_path SCHEMA_NAME do assert_nothing_raised { @connection.remove_index "things", name: "things_Index" } end end def test_remove_index_when_schema_specified @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" assert_nothing_raised { @connection.remove_index "things", name: "#{SCHEMA_NAME}.things_Index" } @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" assert_nothing_raised { @connection.remove_index "#{SCHEMA_NAME}.things", name: "things_Index" } @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" assert_nothing_raised { @connection.remove_index "#{SCHEMA_NAME}.things", name: "#{SCHEMA_NAME}.things_Index" } @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" assert_raises(ArgumentError) { @connection.remove_index "#{SCHEMA2_NAME}.things", name: "#{SCHEMA_NAME}.things_Index" } end def test_primary_key_with_schema_specified [ %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}"), %(#{SCHEMA_NAME}."#{PK_TABLE_NAME}"), %(#{SCHEMA_NAME}.#{PK_TABLE_NAME}) ].each do |given| assert_equal "id", @connection.primary_key(given), "primary key should be found when table referenced as #{given}" end end def test_primary_key_assuming_schema_search_path with_schema_search_path(SCHEMA_NAME) do assert_equal "id", @connection.primary_key(PK_TABLE_NAME), "primary key should be found" end end def test_primary_key_raises_error_if_table_not_found_on_schema_search_path with_schema_search_path(SCHEMA2_NAME) do assert_raises(ActiveRecord::StatementInvalid) do @connection.primary_key(PK_TABLE_NAME) end end end def test_pk_and_sequence_for_with_schema_specified pg_name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name [ %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}"), %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}") ].each do |given| pk, seq = @connection.pk_and_sequence_for(given) assert_equal "id", pk, "primary key should be found when table referenced as #{given}" assert_equal pg_name.new(SCHEMA_NAME, "#{PK_TABLE_NAME}_id_seq"), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}") assert_equal pg_name.new(SCHEMA_NAME, UNMATCHED_SEQUENCE_NAME), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}") end end def test_current_schema { %('$user',public) => "public", SCHEMA_NAME => SCHEMA_NAME, %(#{SCHEMA2_NAME},#{SCHEMA_NAME},public) => SCHEMA2_NAME, %(public,#{SCHEMA2_NAME},#{SCHEMA_NAME}) => "public" }.each do |given, expect| with_schema_search_path(given) { assert_equal expect, @connection.current_schema } end end def test_prepared_statements_with_multiple_schemas [SCHEMA_NAME, SCHEMA2_NAME].each do |schema_name| with_schema_search_path schema_name do Thing5.create(id: 1, name: "thing inside #{SCHEMA_NAME}", email: "thing1@localhost", moment: Time.now) end end [SCHEMA_NAME, SCHEMA2_NAME].each do |schema_name| with_schema_search_path schema_name do assert_equal 1, Thing5.count end end end def test_schema_exists? { "public" => true, SCHEMA_NAME => true, SCHEMA2_NAME => true, "darkside" => false }.each do |given, expect| assert_equal expect, @connection.schema_exists?(given) end end def test_reset_pk_sequence sequence_name = "#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}" @connection.execute "SELECT setval('#{sequence_name}', 123)" assert_equal 124, @connection.select_value("SELECT nextval('#{sequence_name}')") @connection.reset_pk_sequence!("#{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME}") assert_equal 1, @connection.select_value("SELECT nextval('#{sequence_name}')") end def test_set_pk_sequence table_name = "#{SCHEMA_NAME}.#{PK_TABLE_NAME}" _, sequence_name = @connection.pk_and_sequence_for table_name @connection.set_pk_sequence! table_name, 123 assert_equal 124, @connection.select_value("SELECT nextval('#{sequence_name}')") @connection.reset_pk_sequence! table_name end private def columns(table_name) @connection.send(:column_definitions, table_name).map do |name, type, default| "#{name} #{type}" + (default ? " default #{default}" : "") end end def do_dump_index_tests_for_schema(this_schema_name, first_index_column_name, second_index_column_name, third_index_column_name, fourth_index_column_name) with_schema_search_path(this_schema_name) do indexes = @connection.indexes(TABLE_NAME).sort_by(&:name) assert_equal 5, indexes.size index_a, index_b, index_c, index_d, index_e = indexes do_dump_index_assertions_for_one_index(index_a, INDEX_A_NAME, first_index_column_name) do_dump_index_assertions_for_one_index(index_b, INDEX_B_NAME, second_index_column_name) do_dump_index_assertions_for_one_index(index_d, INDEX_D_NAME, third_index_column_name) do_dump_index_assertions_for_one_index(index_e, INDEX_E_NAME, fourth_index_column_name) assert_equal :btree, index_a.using assert_equal :btree, index_b.using assert_equal :gin, index_c.using assert_equal :btree, index_d.using assert_equal :gin, index_e.using assert_equal :desc, index_d.orders[INDEX_D_COLUMN] end end def do_dump_index_assertions_for_one_index(this_index, this_index_name, this_index_column) assert_equal TABLE_NAME, this_index.table assert_equal 1, this_index.columns.size assert_equal this_index_column, this_index.columns[0] assert_equal this_index_name, this_index.name end def bind_param(value) ActiveRecord::Relation::QueryAttribute.new(nil, value, ActiveRecord::Type::Value.new) end end class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper setup do @connection = ActiveRecord::Base.connection end def test_dump_foreign_key_targeting_different_schema @connection.create_schema "my_schema" @connection.create_table "my_schema.trains" do |t| t.string :name end @connection.create_table "wagons" do |t| t.integer :train_id end @connection.add_foreign_key "wagons", "my_schema.trains", column: "train_id" output = dump_table_schema "wagons" assert_match %r{\s+add_foreign_key "wagons", "my_schema\.trains", column: "train_id"$}, output ensure @connection.drop_table "wagons", if_exists: true @connection.drop_table "my_schema.trains", if_exists: true @connection.drop_schema "my_schema", if_exists: true end end class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCase setup do @connection = ActiveRecord::Base.connection @connection.drop_schema "schema_1", if_exists: true @connection.execute "CREATE SCHEMA schema_1" @connection.execute "CREATE DOMAIN schema_1.text AS text" @connection.execute "CREATE DOMAIN schema_1.varchar AS varchar" @connection.execute "CREATE DOMAIN schema_1.bpchar AS bpchar" @old_search_path = @connection.schema_search_path @connection.schema_search_path = "schema_1, pg_catalog" @connection.create_table "defaults" do |t| t.text "text_col", default: "some value" t.string "string_col", default: "some value" t.decimal "decimal_col", default: "3.14159265358979323846" end Default.reset_column_information end teardown do @connection.schema_search_path = @old_search_path @connection.drop_schema "schema_1", if_exists: true Default.reset_column_information end def test_text_defaults_in_new_schema_when_overriding_domain assert_equal "some value", Default.new.text_col, "Default of text column was not correctly parsed" end def test_string_defaults_in_new_schema_when_overriding_domain assert_equal "some value", Default.new.string_col, "Default of string column was not correctly parsed" end def test_decimal_defaults_in_new_schema_when_overriding_domain assert_equal BigDecimal.new("3.14159265358979323846"), Default.new.decimal_col, "Default of decimal column was not correctly parsed" end def test_bpchar_defaults_in_new_schema_when_overriding_domain @connection.execute "ALTER TABLE defaults ADD bpchar_col bpchar DEFAULT 'some value'" Default.reset_column_information assert_equal "some value", Default.new.bpchar_col, "Default of bpchar column was not correctly parsed" end def test_text_defaults_after_updating_column_default @connection.execute "ALTER TABLE defaults ALTER COLUMN text_col SET DEFAULT 'some text'::schema_1.text" assert_equal "some text", Default.new.text_col, "Default of text column was not correctly parsed after updating default using '::text' since postgreSQL will add parens to the default in db" end def test_default_containing_quote_and_colons @connection.execute "ALTER TABLE defaults ALTER COLUMN string_col SET DEFAULT 'foo''::bar'" assert_equal "foo'::bar", Default.new.string_col end end class SchemaWithDotsTest < ActiveRecord::PostgreSQLTestCase include PGSchemaHelper self.use_transactional_tests = false setup do @connection = ActiveRecord::Base.connection @connection.create_schema "my.schema" end teardown do @connection.drop_schema "my.schema", if_exists: true end test "rename_table" do with_schema_search_path('"my.schema"') do @connection.create_table :posts @connection.rename_table :posts, :articles assert_equal ["articles"], @connection.tables end end test "Active Record basics" do with_schema_search_path('"my.schema"') do @connection.create_table :articles do |t| t.string :title end article_class = Class.new(ActiveRecord::Base) do self.table_name = '"my.schema".articles' end article_class.create!(title: "zOMG, welcome to my blorgh!") welcome_article = article_class.last assert_equal "zOMG, welcome to my blorgh!", welcome_article.title end end end