flipper-active_record 1.0.0 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f11ba9f2e7fa94ab06ddddfc753ccb1b398ffc19023ab6b2f5a980ccb55bd974
4
- data.tar.gz: 2566b337548372d8d080f732dbb2824b771f5c58e429ac350ceb0afb5455fea8
3
+ metadata.gz: 8e57f7c82518727c259e650255afa1d8efd820e903f77e6c674e023bfdc7d127
4
+ data.tar.gz: a2c9efa80d93b1f4b110eb63aa7b52bede748dd434ea4833cc41aa3833482930
5
5
  SHA512:
6
- metadata.gz: 40e29c142239ff3a5f474f2f0dd6019d3bc60fcc4aaa76021715cf1dcb241c50c2d2dbf1e669cd5b2efcfa5f6740b40bea477988e25d6b5445ff4fcddd111173
7
- data.tar.gz: 6d9c313e0402d1bd197a1abc17a1aa930296f8f854c132773bebc08c6dbf5fab49a2978a4df196267ca0e29af8b5277868458ba4a7d7a35feb4efdcdb44ed1e0
6
+ metadata.gz: a14d913ed677ffbbeca3d2adc45ca704009e4e214b152a5758d5936ef42a2e9419b41e6fdbf9d2a704a61a2758f9add7d5a4d1da56dd08a71a5be42487e654a2
7
+ data.tar.gz: 7909e3048c672f61c2a92ac2a639cb301615178748b2e97fc4c2885161bce81b21977ccc7ac67b7c61edf173189f87dc391062e25cdf4125a4e4ffbb9925a8b4
@@ -6,10 +6,13 @@ require 'benchmark/ips'
6
6
 
7
7
  flipper = Flipper.new(Flipper::Adapters::ActiveRecord.new)
8
8
 
9
- 2000.times do |i|
10
- flipper.enable_actor :foo, Flipper::Actor.new("User;#{i}")
9
+ 10.times do |n|
10
+ 2000.times do |i|
11
+ flipper.enable_actor 'feature' + n.to_s, Flipper::Actor.new("User;#{i}")
12
+ end
11
13
  end
12
14
 
13
15
  Benchmark.ips do |x|
14
16
  x.report("get_all") { flipper.preload_all }
17
+ x.report("features") { flipper.features }
15
18
  end
@@ -28,3 +28,18 @@ SQL
28
28
  ActiveRecord::Base.connection.execute <<-SQL
29
29
  CREATE UNIQUE INDEX index_gates_on_keys_and_value on flipper_gates (feature_key, key, value)
30
30
  SQL
31
+
32
+ ActiveRecord::Base.connection.execute <<-SQL
33
+ CREATE TABLE users (
34
+ id integer PRIMARY KEY,
35
+ name string NOT NULL,
36
+ influencer boolean,
37
+ created_at datetime NOT NULL,
38
+ updated_at datetime NOT NULL
39
+ )
40
+ SQL
41
+
42
+ require 'flipper/model/active_record'
43
+ class User < ActiveRecord::Base
44
+ include Flipper::Model::ActiveRecord
45
+ end
@@ -0,0 +1,68 @@
1
+ # This is an example script that shows how to migrate from a bunch of individual
2
+ # actors to a group. Should be useful for those who have ended up with large
3
+ # actor sets and want to slim them down for performance reasons.
4
+
5
+ require_relative "./ar_setup"
6
+ require 'flipper/adapters/active_record'
7
+ require 'active_support/all'
8
+
9
+ # 1. enable feature for 100 actors, make 80 influencers
10
+ users = 100.times.map do |n|
11
+ influencer = n < 80 ? true : false
12
+ user = User.create(name: n, influencer: influencer)
13
+ Flipper.enable :stats, user
14
+ user
15
+ end
16
+
17
+ # check enabled, should all be because individual actors are enabled
18
+ print 'Should be [[true, 100]]: '
19
+ print users.group_by { |user| Flipper.enabled?(:stats, user) }.map { |result, users| [result, users.size]}
20
+ puts
21
+
22
+ # 2. register a group so flipper knows what to do with it
23
+ Flipper.register(:influencers) do |actor, context|
24
+ actor.respond_to?(:influencer) && actor.influencer
25
+ end
26
+
27
+ # 3. enable group for feature, THIS IS IMPORTANT
28
+ Flipper.enable :stats, :influencers
29
+
30
+ # check enabled again, should all still be true because individual actors are
31
+ # enabled, but also the group gate would return true for 80 influencers. At this
32
+ # point, it's kind of double true but flipper just cares if any gate returns true.
33
+ print 'Should be [[true, 100]]: '
34
+ print users.group_by { |user| Flipper.enabled?(:stats, user) }.map { |result, users| [result, users.size]}
35
+ puts
36
+
37
+ # 4. now we want to clean up the actors that are covered by the group to slim down
38
+ # the actor set size. So we loop through actors and remove them if group returns
39
+ # true for the provided actor and context.
40
+ Flipper[:stats].actors_value.each do |flipper_id|
41
+ # Hydrate the flipper_id into an active record object. Modify this based on
42
+ # your flipper_id's if you use anything other than active record models and
43
+ # the default flipper_id provided by flipper.
44
+ class_name, id = flipper_id.split(';')
45
+ klass = class_name.constantize
46
+ user = klass.find(id)
47
+
48
+ # if user is in group then disable for actor because they'll still get the feature
49
+ context = Flipper::FeatureCheckContext.new(
50
+ feature_name: :stats,
51
+ values: Flipper[:stats].gate_values,
52
+ actors: [Flipper::Types::Actor.wrap(user)]
53
+ )
54
+
55
+ if Flipper::Gates::Group.new.open?(context)
56
+ Flipper.disable(:stats, user)
57
+ end
58
+ end
59
+
60
+ # check enabled again, should be the same result as previous checks
61
+ print 'Should be [[true, 100]]: '
62
+ print users.group_by { |user| Flipper.enabled?(:stats, user) }.map { |result, users| [result, users.size]}
63
+ puts
64
+
65
+ puts "Actors enabled: #{Flipper[:stats].actors_value.size}"
66
+ puts "Groups enabled: #{Flipper[:stats].groups_value.size}"
67
+
68
+ puts "All actors that could be migrated to groups were migrated. Yay!"
@@ -1,15 +1,22 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  require File.expand_path('../lib/flipper/version', __FILE__)
3
3
  require File.expand_path('../lib/flipper/metadata', __FILE__)
4
+ require "set"
5
+
6
+ # Files that should exist in main flipper gem.
7
+ main_flipper_active_record_files = Set[
8
+ "lib/flipper/model/active_record.rb",
9
+ "spec/flipper/model/active_record_spec.rb",
10
+ ]
4
11
 
5
12
  flipper_active_record_files = lambda do |file|
6
- file =~ /active_record/
13
+ file =~ /active_record/ && !main_flipper_active_record_files.include?(file)
7
14
  end
8
15
 
9
16
  Gem::Specification.new do |gem|
10
17
  gem.authors = ['John Nunemaker']
11
18
  gem.email = 'support@flippercloud.io'
12
- gem.summary = 'ActiveRecord adapter for Flipper'
19
+ gem.summary = 'ActiveRecord feature flag adapter for Flipper'
13
20
  gem.license = 'MIT'
14
21
  gem.homepage = 'https://www.flippercloud.io/docs/adapters/active-record'
15
22
 
@@ -25,5 +32,5 @@ Gem::Specification.new do |gem|
25
32
  gem.metadata = Flipper::METADATA
26
33
 
27
34
  gem.add_dependency 'flipper', "~> #{Flipper::VERSION}"
28
- gem.add_dependency 'activerecord', '>= 4.2', '< 8'
35
+ gem.add_dependency 'activerecord', '>= 4.2', '< 9'
29
36
  end
@@ -1,4 +1,5 @@
1
1
  require 'set'
2
+ require 'securerandom'
2
3
  require 'flipper'
3
4
  require 'active_record'
4
5
 
@@ -7,31 +8,41 @@ module Flipper
7
8
  class ActiveRecord
8
9
  include ::Flipper::Adapter
9
10
 
10
- # Abstract base class for internal models
11
- class Model < ::ActiveRecord::Base
12
- self.abstract_class = true
13
- end
11
+ ActiveSupport.on_load(:active_record) do
12
+ class Model < ::ActiveRecord::Base
13
+ self.abstract_class = true
14
+ end
14
15
 
15
- # Private: Do not use outside of this adapter.
16
- class Feature < Model
17
- self.table_name = [
18
- Model.table_name_prefix,
19
- "flipper_features",
20
- Model.table_name_suffix,
21
- ].join
22
- end
16
+ # Private: Do not use outside of this adapter.
17
+ class Feature < Model
18
+ self.table_name = [
19
+ Model.table_name_prefix,
20
+ "flipper_features",
21
+ Model.table_name_suffix,
22
+ ].join
23
+
24
+ has_many :gates, foreign_key: "feature_key", primary_key: "key"
23
25
 
24
- # Private: Do not use outside of this adapter.
25
- class Gate < Model
26
- self.table_name = [
27
- Model.table_name_prefix,
28
- "flipper_gates",
29
- Model.table_name_suffix,
30
- ].join
26
+ validates :key, presence: true
27
+ end
28
+
29
+ # Private: Do not use outside of this adapter.
30
+ class Gate < Model
31
+ self.table_name = [
32
+ Model.table_name_prefix,
33
+ "flipper_gates",
34
+ Model.table_name_suffix,
35
+ ].join
36
+
37
+ validates :feature_key, presence: true
38
+ validates :key, presence: true
39
+ end
31
40
  end
32
41
 
33
- # Public: The name of the adapter.
34
- attr_reader :name
42
+ VALUE_TO_TEXT_WARNING = <<-EOS
43
+ Your database needs to be migrated to use the latest Flipper features.
44
+ Run `rails generate flipper:update` and `rails db:migrate`.
45
+ EOS
35
46
 
36
47
  # Public: Initialize a new ActiveRecord adapter instance.
37
48
  #
@@ -47,26 +58,27 @@ module Flipper
47
58
  # can roll your own tables and what not, if you so desire.
48
59
  def initialize(options = {})
49
60
  @name = options.fetch(:name, :active_record)
50
- @feature_class = options.fetch(:feature_class) { Feature }
51
- @gate_class = options.fetch(:gate_class) { Gate }
61
+ @feature_class = options.fetch(:feature_class) { Flipper::Adapters::ActiveRecord::Feature }
62
+ @gate_class = options.fetch(:gate_class) { Flipper::Adapters::ActiveRecord::Gate }
52
63
  end
53
64
 
54
65
  # Public: The set of known features.
55
66
  def features
56
- with_connection(@feature_class) { @feature_class.all.map(&:key).to_set }
67
+ with_connection(@feature_class) { @feature_class.distinct.pluck(:key).to_set }
57
68
  end
58
69
 
59
70
  # Public: Adds a feature to the set of known features.
60
71
  def add(feature)
61
72
  with_connection(@feature_class) do
62
- # race condition, but add is only used by enable/disable which happen
63
- # super rarely, so it shouldn't matter in practice
64
- @feature_class.transaction do
65
- unless @feature_class.where(key: feature.key).first
66
- begin
67
- @feature_class.create! { |f| f.key = feature.key }
68
- rescue ::ActiveRecord::RecordNotUnique
73
+ @feature_class.transaction(requires_new: true) do
74
+ begin
75
+ # race condition, but add is only used by enable/disable which happen
76
+ # super rarely, so it shouldn't matter in practice
77
+ unless @feature_class.where(key: feature.key).exists?
78
+ @feature_class.create!(key: feature.key)
69
79
  end
80
+ rescue ::ActiveRecord::RecordNotUnique
81
+ # already added
70
82
  end
71
83
  end
72
84
  end
@@ -117,14 +129,14 @@ module Flipper
117
129
  end
118
130
 
119
131
  def get_all
120
- with_connection(@feature_class) do
132
+ with_connection(@feature_class) do |connection|
121
133
  # query the gates from the db in a single query
122
134
  features = ::Arel::Table.new(@feature_class.table_name.to_sym)
123
135
  gates = ::Arel::Table.new(@gate_class.table_name.to_sym)
124
136
  rows_query = features.join(gates, ::Arel::Nodes::OuterJoin)
125
137
  .on(features[:key].eq(gates[:feature_key]))
126
138
  .project(features[:key].as('feature_key'), gates[:key], gates[:value])
127
- gates = @feature_class.connection.select_rows(rows_query)
139
+ gates = connection.select_rows(rows_query)
128
140
 
129
141
  # group the gates by feature key
130
142
  grouped_gates = gates.inject({}) do |hash, (feature_key, key, value)|
@@ -156,6 +168,8 @@ module Flipper
156
168
  set(feature, gate, thing, clear: true)
157
169
  when :integer
158
170
  set(feature, gate, thing)
171
+ when :json
172
+ set(feature, gate, thing, json: true)
159
173
  when :set
160
174
  enable_multi(feature, gate, thing)
161
175
  else
@@ -178,6 +192,8 @@ module Flipper
178
192
  clear(feature)
179
193
  when :integer
180
194
  set(feature, gate, thing)
195
+ when :json
196
+ delete(feature, gate)
181
197
  when :set
182
198
  with_connection(@gate_class) do
183
199
  @gate_class.where(feature_key: feature.key, key: gate.key, value: thing.value).destroy_all
@@ -198,15 +214,19 @@ module Flipper
198
214
 
199
215
  def set(feature, gate, thing, options = {})
200
216
  clear_feature = options.fetch(:clear, false)
217
+ json_feature = options.fetch(:json, false)
218
+
219
+ raise VALUE_TO_TEXT_WARNING if json_feature && value_not_text?
220
+
201
221
  with_connection(@gate_class) do
202
- @gate_class.transaction do
222
+ @gate_class.transaction(requires_new: true) do
203
223
  clear(feature) if clear_feature
204
- @gate_class.where(feature_key: feature.key, key: gate.key).destroy_all
224
+ delete(feature, gate)
205
225
  begin
206
226
  @gate_class.create! do |g|
207
227
  g.feature_key = feature.key
208
228
  g.key = gate.key
209
- g.value = thing.value.to_s
229
+ g.value = json_feature ? Typecast.to_json(thing.value) : thing.value.to_s
210
230
  end
211
231
  rescue ::ActiveRecord::RecordNotUnique
212
232
  # assume this happened concurrently with the same thing and its fine
@@ -218,18 +238,26 @@ module Flipper
218
238
  nil
219
239
  end
220
240
 
241
+ def delete(feature, gate)
242
+ @gate_class.where(feature_key: feature.key, key: gate.key).destroy_all
243
+ end
244
+
221
245
  def enable_multi(feature, gate, thing)
222
- with_connection(@gate_class) do
223
- @gate_class.create! do |g|
224
- g.feature_key = feature.key
225
- g.key = gate.key
226
- g.value = thing.value.to_s
246
+ with_connection(@gate_class) do |connection|
247
+ begin
248
+ connection.transaction(requires_new: true) do
249
+ @gate_class.create! do |g|
250
+ g.feature_key = feature.key
251
+ g.key = gate.key
252
+ g.value = thing.value.to_s
253
+ end
254
+ end
255
+ rescue ::ActiveRecord::RecordNotUnique
256
+ # already added so move on with life
227
257
  end
228
258
  end
229
259
 
230
260
  nil
231
- rescue ::ActiveRecord::RecordNotUnique
232
- # already added so no need move on with life
233
261
  end
234
262
 
235
263
  def result_for_gates(feature, gates)
@@ -238,13 +266,13 @@ module Flipper
238
266
  feature.gates.each do |gate|
239
267
  result[gate.key] =
240
268
  case gate.data_type
241
- when :boolean
269
+ when :boolean, :integer
242
270
  if row = gates.detect { |key, value| !key.nil? && key.to_sym == gate.key }
243
271
  row.last
244
272
  end
245
- when :integer
273
+ when :json
246
274
  if row = gates.detect { |key, value| !key.nil? && key.to_sym == gate.key }
247
- row.last
275
+ Typecast.from_json(row.last)
248
276
  end
249
277
  when :set
250
278
  gates.select { |key, value| !key.nil? && key.to_sym == gate.key }.map(&:last).to_set
@@ -255,9 +283,26 @@ module Flipper
255
283
  result
256
284
  end
257
285
 
286
+ # Check if value column is text instead of string
287
+ # See https://github.com/flippercloud/flipper/pull/692
288
+ def value_not_text?
289
+ with_connection(@gate_class) do |connection|
290
+ @gate_class.column_for_attribute(:value).type != :text
291
+ end
292
+ rescue ::ActiveRecord::ActiveRecordError => error
293
+ # If the table doesn't exist, the column doesn't exist either
294
+ warn "#{error.message}. You likely need to run `rails g flipper:active_record` and/or `rails db:migrate`."
295
+ end
296
+
258
297
  def with_connection(model = @feature_class, &block)
298
+ warn VALUE_TO_TEXT_WARNING if !warned_about_value_not_text? && value_not_text?
259
299
  model.connection_pool.with_connection(&block)
260
300
  end
301
+
302
+ def warned_about_value_not_text?
303
+ return @warned_about_value_not_text if defined?(@warned_about_value_not_text)
304
+ @warned_about_value_not_text = true
305
+ end
261
306
  end
262
307
  end
263
308
  end
@@ -1,3 +1,13 @@
1
1
  module Flipper
2
- VERSION = '1.0.0'.freeze
2
+ VERSION = '1.3.6'.freeze
3
+
4
+ REQUIRED_RUBY_VERSION = '2.6'.freeze
5
+ NEXT_REQUIRED_RUBY_VERSION = '3.0'.freeze
6
+
7
+ REQUIRED_RAILS_VERSION = '5.2'.freeze
8
+ NEXT_REQUIRED_RAILS_VERSION = '6.1.0'.freeze
9
+
10
+ def self.deprecated_ruby_version?
11
+ Gem::Version.new(RUBY_VERSION) < Gem::Version.new(NEXT_REQUIRED_RUBY_VERSION)
12
+ end
3
13
  end
@@ -1,5 +1,5 @@
1
1
  class CreateFlipperTables < ActiveRecord::Migration<%= migration_version %>
2
- def self.up
2
+ def up
3
3
  create_table :flipper_features do |t|
4
4
  t.string :key, null: false
5
5
  t.timestamps null: false
@@ -9,13 +9,13 @@ class CreateFlipperTables < ActiveRecord::Migration<%= migration_version %>
9
9
  create_table :flipper_gates do |t|
10
10
  t.string :feature_key, null: false
11
11
  t.string :key, null: false
12
- t.string :value
12
+ t.text :value
13
13
  t.timestamps null: false
14
14
  end
15
- add_index :flipper_gates, [:feature_key, :key, :value], unique: true
15
+ add_index :flipper_gates, [:feature_key, :key, :value], unique: true, length: { value: 255 }
16
16
  end
17
17
 
18
- def self.down
18
+ def down
19
19
  drop_table :flipper_gates
20
20
  drop_table :flipper_features
21
21
  end
@@ -1,181 +1,267 @@
1
- require 'flipper/adapters/active_record'
1
+ SpecHelpers.silence { require 'flipper/adapters/active_record' }
2
2
 
3
3
  # Turn off migration logging for specs
4
4
  ActiveRecord::Migration.verbose = false
5
+ ActiveRecord::Tasks::DatabaseTasks.root = File.dirname(__FILE__)
5
6
 
6
7
  RSpec.describe Flipper::Adapters::ActiveRecord do
7
8
  subject { described_class.new }
8
9
 
9
10
  before(:all) do
10
- ActiveRecord::Base.establish_connection(adapter: 'sqlite3',
11
- database: ':memory:')
11
+ # Eval migration template so we can run migration against each database
12
+ template_path = File.join(File.dirname(__FILE__), '../../../lib/generators/flipper/templates/migration.erb')
13
+ migration = ERB.new(File.read(template_path))
14
+ migration_version = "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
15
+ eval migration.result_with_hash(migration_version: migration_version) # defines CreateFlipperTables
12
16
  end
13
17
 
14
- before(:each) do
15
- ActiveRecord::Base.connection.execute <<-SQL
16
- CREATE TABLE flipper_features (
17
- id integer PRIMARY KEY,
18
- key text NOT NULL UNIQUE,
19
- created_at datetime NOT NULL,
20
- updated_at datetime NOT NULL
21
- )
22
- SQL
23
-
24
- ActiveRecord::Base.connection.execute <<-SQL
25
- CREATE TABLE flipper_gates (
26
- id integer PRIMARY KEY,
27
- feature_key text NOT NULL,
28
- key text NOT NULL,
29
- value text DEFAULT NULL,
30
- created_at datetime NOT NULL,
31
- updated_at datetime NOT NULL
32
- )
33
- SQL
34
-
35
- ActiveRecord::Base.connection.execute <<-SQL
36
- CREATE UNIQUE INDEX index_gates_on_keys_and_value on flipper_gates (feature_key, key, value)
37
- SQL
38
- end
18
+ [
19
+ {
20
+ "adapter" => "sqlite3",
21
+ "database" => ":memory:"
22
+ },
39
23
 
40
- after(:each) do
41
- ActiveRecord::Base.connection.execute("DROP table IF EXISTS `flipper_features`")
42
- ActiveRecord::Base.connection.execute("DROP table IF EXISTS `flipper_gates`")
43
- end
24
+ {
25
+ "adapter" => "mysql2",
26
+ "encoding" => "utf8mb4",
27
+ "host" => ENV["MYSQL_HOST"],
28
+ "username" => ENV["MYSQL_USER"] || "root",
29
+ "password" => ENV["MYSQL_PASSWORD"] || "",
30
+ "database" => ENV["MYSQL_DATABASE"] || "flipper_test",
31
+ "port" => ENV["DB_PORT"] || 3306
32
+ },
44
33
 
45
- it_should_behave_like 'a flipper adapter'
34
+ {
35
+ "adapter" => "postgresql",
36
+ "encoding" => "unicode",
37
+ "host" => "127.0.0.1",
38
+ "username" => ENV["POSTGRES_USER"] || "",
39
+ "password" => ENV["POSTGRES_PASSWORD"] || "",
40
+ "database" => ENV["POSTGRES_DATABASE"] || "flipper_test",
41
+ }
42
+ ].each do |config|
43
+ context "with #{config['adapter']}" do
44
+ context "with tables created" do
45
+ before(:all) do
46
+ skip_on_error(ActiveRecord::ConnectionNotEstablished, "#{config['adapter']} not available") do
47
+ silence do
48
+ ActiveRecord::Tasks::DatabaseTasks.create(config)
49
+ end
50
+ end
46
51
 
47
- context 'requiring "flipper-active_record"' do
48
- before do
49
- Flipper.configuration = nil
50
- Flipper.instance = nil
52
+ Flipper.configuration = nil
53
+ end
51
54
 
52
- load 'flipper/adapters/active_record.rb'
53
- ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base)
54
- end
55
+ before(:each) do
56
+ silence do
57
+ ActiveRecord::Tasks::DatabaseTasks.purge(config)
58
+ CreateFlipperTables.migrate(:up)
59
+ end
60
+ end
55
61
 
56
- it 'configures itself' do
57
- expect(Flipper.adapter.adapter).to be_a(Flipper::Adapters::ActiveRecord)
58
- end
59
- end
62
+ after(:all) do
63
+ silence { ActiveRecord::Tasks::DatabaseTasks.drop(config) } unless $skip
64
+ end
60
65
 
61
- context "ActiveRecord connection_pool" do
62
- before do
63
- ActiveRecord::Base.clear_active_connections!
64
- end
66
+ it_should_behave_like 'a flipper adapter'
65
67
 
66
- context "#features" do
67
- it "does not hold onto connections" do
68
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
69
- subject.features
70
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
71
- end
68
+ it "should load actor ids fine" do
69
+ flipper.enable_percentage_of_time(:foo, 1)
72
70
 
73
- it "does not release previously held connection" do
74
- ActiveRecord::Base.connection # establish a new connection
75
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
76
- subject.features
77
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
78
- end
79
- end
71
+ Flipper::Adapters::ActiveRecord::Gate.create!(
72
+ feature_key: "foo",
73
+ key: "actors",
74
+ value: "Organization;4",
75
+ )
80
76
 
81
- context "#get_all" do
82
- it "does not hold onto connections" do
83
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
84
- subject.get_all
85
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
86
- end
77
+ flipper = Flipper.new(subject)
78
+ flipper.preload([:foo])
79
+ end
87
80
 
88
- it "does not release previously held connection" do
89
- ActiveRecord::Base.connection # establish a new connection
90
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
91
- subject.get_all
92
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
93
- end
94
- end
81
+ it 'should not poison wrapping transactions' do
82
+ flipper = Flipper.new(subject)
95
83
 
96
- context "#add / #remove / #clear" do
97
- let(:feature) { Flipper::Feature.new(:search, subject) }
98
-
99
- it "does not hold onto connections" do
100
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
101
- subject.add(feature)
102
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
103
- subject.remove(feature)
104
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
105
- subject.clear(feature)
106
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
107
- end
84
+ actor = Struct.new(:flipper_id).new('flipper-id-123')
85
+ flipper.enable_actor(:foo, actor)
108
86
 
109
- it "does not release previously held connection" do
110
- ActiveRecord::Base.connection # establish a new connection
111
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
112
- subject.add(feature)
113
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
114
- subject.remove(feature)
115
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
116
- subject.clear(feature)
117
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
118
- end
119
- end
87
+ ActiveRecord::Base.transaction do
88
+ flipper.enable_actor(:foo, actor)
89
+ # any read on the next line is fine, just need to ensure that
90
+ # poisoned transaction isn't raised
91
+ expect(Flipper::Adapters::ActiveRecord::Gate.count).to eq(1)
92
+ end
93
+ end
120
94
 
121
- context "#get_multi" do
122
- let(:feature) { Flipper::Feature.new(:search, subject) }
95
+ context "ActiveRecord connection_pool" do
96
+ before do
97
+ ActiveRecord::Base.connection_handler.clear_active_connections!
98
+ end
123
99
 
124
- it "does not hold onto connections" do
125
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
126
- subject.get_multi([feature])
127
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
128
- end
100
+ context "#features" do
101
+ it "does not hold onto connections" do
102
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
103
+ subject.features
104
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
105
+ end
129
106
 
130
- it "does not release previously held connection" do
131
- ActiveRecord::Base.connection # establish a new connection
132
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
133
- subject.get_multi([feature])
134
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
135
- end
136
- end
107
+ it "does not release previously held connection" do
108
+ ActiveRecord::Base.connection # establish a new connection
109
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
110
+ subject.features
111
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
112
+ end
113
+ end
137
114
 
138
- context "#enable/#disable boolean" do
139
- let(:feature) { Flipper::Feature.new(:search, subject) }
140
- let(:gate) { feature.gate(:boolean)}
115
+ context "#get_all" do
116
+ it "does not hold onto connections" do
117
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
118
+ subject.get_all
119
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
120
+ end
141
121
 
142
- it "does not hold onto connections" do
143
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
144
- subject.enable(feature, gate, gate.wrap(true))
145
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
146
- subject.disable(feature, gate, gate.wrap(false))
147
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
148
- end
122
+ it "does not release previously held connection" do
123
+ ActiveRecord::Base.connection # establish a new connection
124
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
125
+ subject.get_all
126
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
127
+ end
128
+ end
149
129
 
150
- it "does not release previously held connection" do
151
- ActiveRecord::Base.connection # establish a new connection
152
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
153
- subject.enable(feature, gate, gate.wrap(true))
154
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
155
- subject.disable(feature, gate, gate.wrap(false))
156
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
157
- end
158
- end
130
+ context "#add / #remove / #clear" do
131
+ let(:feature) { Flipper::Feature.new(:search, subject) }
132
+
133
+ it "does not hold onto connections" do
134
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
135
+ subject.add(feature)
136
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
137
+ subject.remove(feature)
138
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
139
+ subject.clear(feature)
140
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
141
+ end
142
+
143
+ it "does not release previously held connection" do
144
+ ActiveRecord::Base.connection # establish a new connection
145
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
146
+ subject.add(feature)
147
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
148
+ subject.remove(feature)
149
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
150
+ subject.clear(feature)
151
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
152
+ end
153
+ end
154
+
155
+ context "#get_multi" do
156
+ let(:feature) { Flipper::Feature.new(:search, subject) }
157
+
158
+ it "does not hold onto connections" do
159
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
160
+ subject.get_multi([feature])
161
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
162
+ end
163
+
164
+ it "does not release previously held connection" do
165
+ ActiveRecord::Base.connection # establish a new connection
166
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
167
+ subject.get_multi([feature])
168
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
169
+ end
170
+ end
171
+
172
+ context "#enable/#disable boolean" do
173
+ let(:feature) { Flipper::Feature.new(:search, subject) }
174
+ let(:gate) { feature.gate(:boolean)}
175
+
176
+ it "does not hold onto connections" do
177
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
178
+ subject.enable(feature, gate, gate.wrap(true))
179
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
180
+ subject.disable(feature, gate, gate.wrap(false))
181
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
182
+ end
183
+
184
+ it "does not release previously held connection" do
185
+ ActiveRecord::Base.connection # establish a new connection
186
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
187
+ subject.enable(feature, gate, gate.wrap(true))
188
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
189
+ subject.disable(feature, gate, gate.wrap(false))
190
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
191
+ end
192
+ end
193
+
194
+ context "#enable/#disable set" do
195
+ let(:feature) { Flipper::Feature.new(:search, subject) }
196
+ let(:gate) { feature.gate(:group) }
159
197
 
160
- context "#enable/#disable set" do
161
- let(:feature) { Flipper::Feature.new(:search, subject) }
162
- let(:gate) { feature.gate(:group) }
198
+ it "does not hold onto connections" do
199
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
200
+ subject.enable(feature, gate, gate.wrap(:admin))
201
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
202
+ subject.disable(feature, gate, gate.wrap(:admin))
203
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
204
+ end
163
205
 
164
- it "does not hold onto connections" do
165
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
166
- subject.enable(feature, gate, gate.wrap(:admin))
167
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
168
- subject.disable(feature, gate, gate.wrap(:admin))
169
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
206
+ it "does not release previously held connection" do
207
+ ActiveRecord::Base.connection # establish a new connection
208
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
209
+ subject.enable(feature, gate, gate.wrap(:admin))
210
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
211
+ subject.disable(feature, gate, gate.wrap(:admin))
212
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
213
+ end
214
+ end
215
+ end
216
+
217
+ context 'requiring "flipper-active_record"' do
218
+ before do
219
+ Flipper.configuration = nil
220
+ Flipper.instance = nil
221
+
222
+ silence { load 'flipper/adapters/active_record.rb' }
223
+ end
224
+
225
+ it 'configures itself' do
226
+ expect(Flipper.adapter.adapter).to be_a(Flipper::Adapters::ActiveRecord)
227
+ end
228
+ end
170
229
  end
171
230
 
172
- it "does not release previously held connection" do
173
- ActiveRecord::Base.connection # establish a new connection
174
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
175
- subject.enable(feature, gate, gate.wrap(:admin))
176
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
177
- subject.disable(feature, gate, gate.wrap(:admin))
178
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
231
+ context "without tables created" do
232
+ before(:all) do
233
+ skip_on_error(ActiveRecord::ConnectionNotEstablished, "#{config['adapter']} not available") do
234
+ silence do
235
+ ActiveRecord::Tasks::DatabaseTasks.create(config)
236
+ end
237
+ end
238
+
239
+ Flipper.configuration = nil
240
+ end
241
+
242
+ before(:each) do
243
+ ActiveRecord::Base.establish_connection(config)
244
+ end
245
+
246
+ after(:each) do
247
+ ActiveRecord::Base.connection.close
248
+ end
249
+
250
+ after(:all) do
251
+ silence { ActiveRecord::Tasks::DatabaseTasks.drop(config) } unless $skip
252
+ end
253
+
254
+ it "does not raise an error" do
255
+ Flipper.configuration = nil
256
+ Flipper.instance = nil
257
+
258
+ silence do
259
+ expect {
260
+ load 'flipper/adapters/active_record.rb'
261
+ Flipper::Adapters::ActiveRecord.new
262
+ }.not_to raise_error
263
+ end
264
+ end
179
265
  end
180
266
  end
181
267
  end
@@ -11,8 +11,6 @@ class ActiveRecordTest < MiniTest::Test
11
11
  database: ':memory:')
12
12
 
13
13
  def setup
14
- @adapter = Flipper::Adapters::ActiveRecord.new
15
-
16
14
  ActiveRecord::Base.connection.execute <<-SQL
17
15
  CREATE TABLE flipper_features (
18
16
  id integer PRIMARY KEY,
@@ -36,6 +34,8 @@ class ActiveRecordTest < MiniTest::Test
36
34
  ActiveRecord::Base.connection.execute <<-SQL
37
35
  CREATE UNIQUE INDEX index_gates_on_keys_and_value on flipper_gates (feature_key, key, value)
38
36
  SQL
37
+
38
+ @adapter = Flipper::Adapters::ActiveRecord.new
39
39
  end
40
40
 
41
41
  def teardown
@@ -47,8 +47,7 @@ class ActiveRecordTest < MiniTest::Test
47
47
  ActiveRecord::Base.table_name_prefix = :foo_
48
48
  ActiveRecord::Base.table_name_suffix = :_bar
49
49
 
50
- Flipper::Adapters::ActiveRecord.send(:remove_const, :Feature)
51
- Flipper::Adapters::ActiveRecord.send(:remove_const, :Gate)
50
+ Flipper::Adapters.send(:remove_const, :ActiveRecord)
52
51
  load("flipper/adapters/active_record.rb")
53
52
 
54
53
  assert_equal "foo_flipper_features_bar", Flipper::Adapters::ActiveRecord::Feature.table_name
@@ -58,8 +57,7 @@ class ActiveRecordTest < MiniTest::Test
58
57
  ActiveRecord::Base.table_name_prefix = ""
59
58
  ActiveRecord::Base.table_name_suffix = ""
60
59
 
61
- Flipper::Adapters::ActiveRecord.send(:remove_const, :Feature)
62
- Flipper::Adapters::ActiveRecord.send(:remove_const, :Gate)
60
+ Flipper::Adapters.send(:remove_const, :ActiveRecord)
63
61
  load("flipper/adapters/active_record.rb")
64
62
  end
65
63
  end
@@ -1,6 +1,4 @@
1
1
  require 'helper'
2
- require 'active_record'
3
- require 'rails/generators/test_case'
4
2
  require 'generators/flipper/active_record_generator'
5
3
 
6
4
  class FlipperActiveRecordGeneratorTest < Rails::Generators::TestCase
@@ -17,7 +15,7 @@ class FlipperActiveRecordGeneratorTest < Rails::Generators::TestCase
17
15
  end
18
16
  assert_migration 'db/migrate/create_flipper_tables.rb', <<~MIGRATION
19
17
  class CreateFlipperTables < ActiveRecord::Migration#{migration_version}
20
- def self.up
18
+ def up
21
19
  create_table :flipper_features do |t|
22
20
  t.string :key, null: false
23
21
  t.timestamps null: false
@@ -27,13 +25,13 @@ class FlipperActiveRecordGeneratorTest < Rails::Generators::TestCase
27
25
  create_table :flipper_gates do |t|
28
26
  t.string :feature_key, null: false
29
27
  t.string :key, null: false
30
- t.string :value
28
+ t.text :value
31
29
  t.timestamps null: false
32
30
  end
33
- add_index :flipper_gates, [:feature_key, :key, :value], unique: true
31
+ add_index :flipper_gates, [:feature_key, :key, :value], unique: true, length: { value: 255 }
34
32
  end
35
33
 
36
- def self.down
34
+ def down
37
35
  drop_table :flipper_gates
38
36
  drop_table :flipper_features
39
37
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flipper-active_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-08-23 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: flipper
@@ -16,14 +15,14 @@ dependencies:
16
15
  requirements:
17
16
  - - "~>"
18
17
  - !ruby/object:Gem::Version
19
- version: 1.0.0
18
+ version: 1.3.6
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - "~>"
25
24
  - !ruby/object:Gem::Version
26
- version: 1.0.0
25
+ version: 1.3.6
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: activerecord
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -33,7 +32,7 @@ dependencies:
33
32
  version: '4.2'
34
33
  - - "<"
35
34
  - !ruby/object:Gem::Version
36
- version: '8'
35
+ version: '9'
37
36
  type: :runtime
38
37
  prerelease: false
39
38
  version_requirements: !ruby/object:Gem::Requirement
@@ -43,8 +42,7 @@ dependencies:
43
42
  version: '4.2'
44
43
  - - "<"
45
44
  - !ruby/object:Gem::Version
46
- version: '8'
47
- description:
45
+ version: '9'
48
46
  email: support@flippercloud.io
49
47
  executables: []
50
48
  extensions: []
@@ -56,6 +54,7 @@ files:
56
54
  - examples/active_record/ar_setup.rb
57
55
  - examples/active_record/basic.rb
58
56
  - examples/active_record/cached.rb
57
+ - examples/active_record/group_migration.rb
59
58
  - examples/active_record/internals.rb
60
59
  - flipper-active_record.gemspec
61
60
  - lib/flipper-active_record.rb
@@ -74,8 +73,8 @@ metadata:
74
73
  homepage_uri: https://www.flippercloud.io
75
74
  source_code_uri: https://github.com/flippercloud/flipper
76
75
  bug_tracker_uri: https://github.com/flippercloud/flipper/issues
77
- changelog_uri: https://github.com/flippercloud/flipper/blob/main/Changelog.md
78
- post_install_message:
76
+ changelog_uri: https://github.com/flippercloud/flipper/releases/tag/v1.3.6
77
+ funding_uri: https://github.com/sponsors/flippercloud
79
78
  rdoc_options: []
80
79
  require_paths:
81
80
  - lib
@@ -90,10 +89,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
89
  - !ruby/object:Gem::Version
91
90
  version: '0'
92
91
  requirements: []
93
- rubygems_version: 3.4.10
94
- signing_key:
92
+ rubygems_version: 3.6.9
95
93
  specification_version: 4
96
- summary: ActiveRecord adapter for Flipper
94
+ summary: ActiveRecord feature flag adapter for Flipper
97
95
  test_files:
98
96
  - spec/flipper/adapters/active_record_spec.rb
99
97
  - test/adapters/active_record_test.rb