Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions lib/flipper/adapters/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion lib/generators/flipper/active_record_generator.rb
Original file line number Diff line number Diff line change
@@ -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')
Expand All @@ -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
Expand Down
44 changes: 44 additions & 0 deletions lib/generators/flipper/table_prefix.rb
Original file line number Diff line number Diff line change
@@ -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
20 changes: 13 additions & 7 deletions lib/generators/flipper/templates/migration.erb
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions lib/generators/flipper/update_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require 'rails/generators'
require 'rails/generators/active_record'
require 'generators/flipper/table_prefix'

module Flipper
module Generators
Expand All @@ -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
Expand All @@ -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
Expand Down
40 changes: 40 additions & 0 deletions test/adapters/active_record_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions test_rails/generators/flipper/active_record_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
32 changes: 32 additions & 0 deletions test_rails/generators/flipper/update_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down