diff --git a/lib/flipper/adapters/active_record.rb b/lib/flipper/adapters/active_record.rb index 9617574be..bd738f78b 100644 --- a/lib/flipper/adapters/active_record.rb +++ b/lib/flipper/adapters/active_record.rb @@ -24,6 +24,7 @@ class ActiveRecord # name - The Symbol name for this adapter. Optional (default :active_record) # feature_class - The AR class responsible for the features table. # gate_class - The AR class responsible for the gates table. + # table_prefix - The prefix for the default features and gates tables. # # Allowing the overriding of name is so you can differentiate multiple # instances of this adapter from each other, if, for some reason, that is @@ -33,8 +34,13 @@ class ActiveRecord # can roll your own tables and what not, if you so desire. def initialize(options = {}) @name = options.fetch(:name, :active_record) - @feature_class = options.fetch(:feature_class) { Flipper::Adapters::ActiveRecord::Feature } - @gate_class = options.fetch(:gate_class) { Flipper::Adapters::ActiveRecord::Gate } + table_prefix = options[:table_prefix] + @feature_class = options.fetch(:feature_class) do + model_class(Flipper::Adapters::ActiveRecord::Feature, table_prefix, "flipper_features") + end + @gate_class = options.fetch(:gate_class) do + model_class(Flipper::Adapters::ActiveRecord::Gate, table_prefix, "flipper_gates") + end end # Public: The set of known features. @@ -190,6 +196,14 @@ def unsupported_data_type(data_type) private + def model_class(default_class, table_prefix, table_name) + return default_class if table_prefix.nil? + + Class.new(default_class) do + self.table_name = "#{table_prefix}#{table_name}" + end + end + def set(feature, gate, thing, options = {}) clear_feature = options.fetch(:clear, false) json_feature = options.fetch(:json, false) diff --git a/lib/generators/flipper/active_record_generator.rb b/lib/generators/flipper/active_record_generator.rb index 13751e0f2..540a4e3fb 100644 --- a/lib/generators/flipper/active_record_generator.rb +++ b/lib/generators/flipper/active_record_generator.rb @@ -1,9 +1,11 @@ require 'rails/generators/active_record' +require 'generators/flipper/table_prefix' module Flipper module Generators class ActiveRecordGenerator < ::Rails::Generators::Base include ::Rails::Generators::Migration + include Flipper::Generators::TablePrefix desc 'Generates migration for flipper tables' source_paths << File.join(File.dirname(__FILE__), 'templates') @@ -24,7 +26,8 @@ def create_migration_file options = { migration_version: migration_version, } - migration_template 'migration.erb', 'db/migrate/create_flipper_tables.rb', options + destination = File.join('db/migrate', prefixed_migration_file_name('create_flipper_tables.rb')) + migration_template 'migration.erb', destination, options end def migration_version diff --git a/lib/generators/flipper/table_prefix.rb b/lib/generators/flipper/table_prefix.rb new file mode 100644 index 000000000..66a29c944 --- /dev/null +++ b/lib/generators/flipper/table_prefix.rb @@ -0,0 +1,44 @@ +require 'active_support/core_ext/string/inflections' + +module Flipper + module Generators + module TablePrefix + def self.included(generator) + generator.class_option :table_prefix, + type: :string, + default: '', + desc: 'Prefix for the flipper_features and flipper_gates tables' + end + + private + + def feature_table_name + :"#{table_prefix}flipper_features" + end + + def gate_table_name + :"#{table_prefix}flipper_gates" + end + + def prefixed_migration_file_name(file_name) + file_name.sub('flipper', "#{table_prefix}flipper") + end + + def migration_class_name(class_name) + class_name.sub('Flipper', "#{table_prefix.camelize}Flipper") + end + + def feature_index_name + :"index_#{table_prefix}flipper_features_on_key" unless table_prefix.empty? + end + + def gate_index_name + :"index_#{table_prefix}flipper_gates_on_keys" unless table_prefix.empty? + end + + def table_prefix + options[:table_prefix].to_s + end + end + end +end diff --git a/lib/generators/flipper/templates/migration.erb b/lib/generators/flipper/templates/migration.erb index 178f0356b..3e9f22db1 100644 --- a/lib/generators/flipper/templates/migration.erb +++ b/lib/generators/flipper/templates/migration.erb @@ -1,22 +1,28 @@ -class CreateFlipperTables < ActiveRecord::Migration<%= migration_version %> +<% +migration_class = respond_to?(:migration_class_name, true) ? migration_class_name('CreateFlipperTables') : 'CreateFlipperTables' +feature_table = respond_to?(:feature_table_name, true) ? feature_table_name : :flipper_features +gate_table = respond_to?(:gate_table_name, true) ? gate_table_name : :flipper_gates +feature_index = respond_to?(:feature_index_name, true) ? feature_index_name : nil +gate_index = respond_to?(:gate_index_name, true) ? gate_index_name : nil +%>class <%= migration_class %> < ActiveRecord::Migration<%= migration_version %> def up - create_table :flipper_features do |t| + create_table <%= feature_table.inspect %> do |t| t.string :key, null: false t.timestamps null: false end - add_index :flipper_features, :key, unique: true + add_index <%= feature_table.inspect %>, :key, unique: true<%= ", name: #{feature_index.inspect}" if feature_index %> - create_table :flipper_gates do |t| + create_table <%= gate_table.inspect %> do |t| t.string :feature_key, null: false t.string :key, null: false t.text :value t.timestamps null: false end - add_index :flipper_gates, [:feature_key, :key, :value], unique: true, length: { value: 255 } + add_index <%= gate_table.inspect %>, [:feature_key, :key, :value], unique: true, length: { value: 255 }<%= ", name: #{gate_index.inspect}" if gate_index %> end def down - drop_table :flipper_gates - drop_table :flipper_features + drop_table <%= gate_table.inspect %> + drop_table <%= feature_table.inspect %> end end diff --git a/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb b/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb index 6ee99bd22..c6be64d5f 100644 --- a/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +++ b/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb @@ -1,22 +1,28 @@ -class CreateFlipperTables < ActiveRecord::Migration<%= migration_version %> +<% +migration_class = migration_class_name('CreateFlipperTables') +feature_table = feature_table_name +gate_table = gate_table_name +feature_index = feature_index_name +gate_index = gate_index_name +%>class <%= migration_class %> < ActiveRecord::Migration<%= migration_version %> def up - create_table :flipper_features do |t| + create_table <%= feature_table.inspect %> do |t| t.string :key, null: false t.timestamps null: false end - add_index :flipper_features, :key, unique: true + add_index <%= feature_table.inspect %>, :key, unique: true<%= ", name: #{feature_index.inspect}" if feature_index %> - create_table :flipper_gates do |t| + create_table <%= gate_table.inspect %> do |t| t.string :feature_key, null: false t.string :key, null: false t.string :value t.timestamps null: false end - add_index :flipper_gates, [:feature_key, :key, :value], unique: true + add_index <%= gate_table.inspect %>, [:feature_key, :key, :value], unique: true<%= ", name: #{gate_index.inspect}" if gate_index %> end def down - drop_table :flipper_gates - drop_table :flipper_features + drop_table <%= gate_table.inspect %> + drop_table <%= feature_table.inspect %> end end diff --git a/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb b/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb index d51115e6f..26476bdf5 100644 --- a/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +++ b/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb @@ -1,18 +1,23 @@ # frozen_string_literal: true -class ChangeFlipperGatesValueToText < ActiveRecord::Migration<%= migration_version %> +<% +migration_class = migration_class_name('ChangeFlipperGatesValueToText') +gate_table = gate_table_name +gate_index = gate_index_name +%>class <%= migration_class %> < ActiveRecord::Migration<%= migration_version %> def up # Ensure this incremental update migration is idempotent - return unless connection.column_exists? :flipper_gates, :value, :string + return unless connection.column_exists? <%= gate_table.inspect %>, :value, :string - if index_exists? :flipper_gates, [:feature_key, :key, :value] - remove_index :flipper_gates, [:feature_key, :key, :value] + gate_index = connection.indexes(<%= gate_table.inspect %>).find do |index| + index.columns == %w[feature_key key value] end - change_column :flipper_gates, :value, :text - add_index :flipper_gates, [:feature_key, :key, :value], unique: true, length: { value: 255 } + remove_index <%= gate_table.inspect %>, name: gate_index.name if gate_index + change_column <%= gate_table.inspect %>, :value, :text + add_index <%= gate_table.inspect %>, [:feature_key, :key, :value], unique: true, length: { value: 255 }<%= ", name: #{gate_index.inspect}" if gate_index %> end def down - change_column :flipper_gates, :value, :string + change_column <%= gate_table.inspect %>, :value, :string end end diff --git a/lib/generators/flipper/update_generator.rb b/lib/generators/flipper/update_generator.rb index f91498529..59fa1e39b 100644 --- a/lib/generators/flipper/update_generator.rb +++ b/lib/generators/flipper/update_generator.rb @@ -2,6 +2,7 @@ require 'rails/generators' require 'rails/generators/active_record' +require 'generators/flipper/table_prefix' module Flipper module Generators @@ -11,6 +12,7 @@ module Generators # class UpdateGenerator < Rails::Generators::Base include ActiveRecord::Generators::Migration + include Flipper::Generators::TablePrefix TEMPLATES = File.join(File.dirname(__FILE__), 'templates/update') source_paths << TEMPLATES @@ -21,6 +23,7 @@ def update_migration_files migration_templates = Dir.children(File.join(TEMPLATES, 'migrations')).sort migration_templates.each do |template_file| destination_file = template_file.match(/^\d*_(.*\.rb)/)[1] # 01_create_flipper_tables.rb.erb => create_flipper_tables.rb + destination_file = prefixed_migration_file_name(destination_file) migration_template "migrations/#{template_file}", File.join(db_migrate_path, destination_file), skip: true end end diff --git a/test/adapters/active_record_test.rb b/test/adapters/active_record_test.rb index 0b3a031ba..ca1e04fd8 100644 --- a/test/adapters/active_record_test.rb +++ b/test/adapters/active_record_test.rb @@ -41,6 +41,46 @@ def setup def teardown ActiveRecord::Base.connection.execute("DROP table IF EXISTS `flipper_features`") ActiveRecord::Base.connection.execute("DROP table IF EXISTS `flipper_gates`") + ActiveRecord::Base.connection.execute("DROP table IF EXISTS `cross_product_flipper_features`") + ActiveRecord::Base.connection.execute("DROP table IF EXISTS `cross_product_flipper_gates`") + end + + def test_table_prefix_uses_internal_model_subclasses + ActiveRecord::Base.connection.execute <<-SQL + CREATE TABLE cross_product_flipper_features ( + id integer PRIMARY KEY, + key text NOT NULL UNIQUE, + created_at datetime NOT NULL, + updated_at datetime NOT NULL + ) + SQL + + ActiveRecord::Base.connection.execute <<-SQL + CREATE TABLE cross_product_flipper_gates ( + id integer PRIMARY KEY, + feature_key text NOT NULL, + key text NOT NULL, + value text DEFAULT NULL, + created_at datetime NOT NULL, + updated_at datetime NOT NULL + ) + SQL + + adapter = Flipper::Adapters::ActiveRecord.new(table_prefix: "cross_product_") + feature_class = adapter.instance_variable_get(:@feature_class) + gate_class = adapter.instance_variable_get(:@gate_class) + + assert_operator feature_class, :<, Flipper::Adapters::ActiveRecord::Feature + assert_operator gate_class, :<, Flipper::Adapters::ActiveRecord::Gate + assert_equal "cross_product_flipper_features", feature_class.table_name + assert_equal "cross_product_flipper_gates", gate_class.table_name + + flipper = Flipper.new(adapter) + flipper[:search].enable + + assert flipper[:search].enabled? + assert_equal ["search"], feature_class.pluck(:key) + assert_equal [["search", "boolean", "true"]], gate_class.pluck(:feature_key, :key, :value) end def test_models_honor_table_name_prefixes_and_suffixes diff --git a/test_rails/generators/flipper/active_record_generator_test.rb b/test_rails/generators/flipper/active_record_generator_test.rb index 2ad7fab87..eb597dac7 100644 --- a/test_rails/generators/flipper/active_record_generator_test.rb +++ b/test_rails/generators/flipper/active_record_generator_test.rb @@ -38,4 +38,37 @@ def down end MIGRATION end + + def test_generates_migration_with_table_prefix + run_generator ["--table-prefix=cross_product_"] + migration_version = if Rails::VERSION::MAJOR.to_i < 5 + "" + else + "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" + end + assert_migration 'db/migrate/create_cross_product_flipper_tables.rb', <<~MIGRATION + class CreateCrossProductFlipperTables < ActiveRecord::Migration#{migration_version} + def up + create_table :cross_product_flipper_features do |t| + t.string :key, null: false + t.timestamps null: false + end + add_index :cross_product_flipper_features, :key, unique: true, name: :index_cross_product_flipper_features_on_key + + create_table :cross_product_flipper_gates do |t| + t.string :feature_key, null: false + t.string :key, null: false + t.text :value + t.timestamps null: false + end + add_index :cross_product_flipper_gates, [:feature_key, :key, :value], unique: true, length: { value: 255 }, name: :index_cross_product_flipper_gates_on_keys + end + + def down + drop_table :cross_product_flipper_gates + drop_table :cross_product_flipper_features + end + end + MIGRATION + end end diff --git a/test_rails/generators/flipper/update_generator_test.rb b/test_rails/generators/flipper/update_generator_test.rb index 6c848c26c..5ec2481c4 100644 --- a/test_rails/generators/flipper/update_generator_test.rb +++ b/test_rails/generators/flipper/update_generator_test.rb @@ -82,10 +82,42 @@ class UpdateGeneratorTest < Rails::Generators::TestCase assert ActiveRecord::Base.connection.column_exists?(:flipper_gates, :value, :text) end + test "generates migrations with a table prefix" do + run_generator ["--table-prefix=cross_product_"] + + assert_migration "db/migrate/create_cross_product_flipper_tables.rb" do |migration| + assert_match(/class CreateCrossProductFlipperTables/, migration) + assert_match(/create_table :cross_product_flipper_features/, migration) + assert_match(/create_table :cross_product_flipper_gates/, migration) + assert_match(/name: :index_cross_product_flipper_gates_on_keys/, migration) + end + + assert_migration "db/migrate/change_cross_product_flipper_gates_value_to_text.rb" do |migration| + assert_match(/class ChangeCrossProductFlipperGatesValueToText/, migration) + assert_match(/column_exists\? :cross_product_flipper_gates/, migration) + assert_match(/connection.indexes\(:cross_product_flipper_gates\)/, migration) + assert_match(/remove_index :cross_product_flipper_gates, name: gate_index.name/, migration) + assert_match(/change_column :cross_product_flipper_gates/, migration) + assert_match(/name: :index_cross_product_flipper_gates_on_keys/, migration) + end + + require_migrations + + silence { CreateCrossProductFlipperTables.migrate(:up) } + assert ActiveRecord::Base.connection.table_exists?(:cross_product_flipper_features) + assert ActiveRecord::Base.connection.table_exists?(:cross_product_flipper_gates) + assert ActiveRecord::Base.connection.column_exists?(:cross_product_flipper_gates, :value, :string) + + silence { ChangeCrossProductFlipperGatesValueToText.migrate(:up) } + assert ActiveRecord::Base.connection.column_exists?(:cross_product_flipper_gates, :value, :text) + end + def require_migrations # If these are not reloaded, then test order can cause failures Object.send(:remove_const, :CreateFlipperTables) if defined?(::CreateFlipperTables) Object.send(:remove_const, :ChangeFlipperGatesValueToText) if defined?(::ChangeFlipperGatesValueToText) + Object.send(:remove_const, :CreateCrossProductFlipperTables) if defined?(::CreateCrossProductFlipperTables) + Object.send(:remove_const, :ChangeCrossProductFlipperGatesValueToText) if defined?(::ChangeCrossProductFlipperGatesValueToText) Dir.glob("#{ROOT}/db/migrate/*.rb").each do |file| assert_nothing_raised do