flipper-active_record 0.28.3 → 1.3.4

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: fc8217d866ba4e5e348943be13ebb76c50d7d90ff0ce39c49b1dfd721d2f6da3
4
- data.tar.gz: 8ba526732ad0786ccfb682fb3732d4226b5f357466cd52a08dbfa3534c853ade
3
+ metadata.gz: ae15abe1a25ef415ec5f7783b24192ae52cbf86b6d8e8026a7b23d3182db2ea3
4
+ data.tar.gz: 92c793efbd0539545f73d363f61df2f3a43d949aafb0bb303ecb5d8a747e660a
5
5
  SHA512:
6
- metadata.gz: 8edffe9b5b277baaea8ea0b81bcc2e28b84b09317bf597865344487a5cb36f7a7117759baf31d9e215459775e64af99d778181fb5bd20764d7afb36071587f0c
7
- data.tar.gz: 77f3fe7f0bb0873f52e5fe4a80b5e8a5beec83dd3baaa73cbeeb90e24f924dce9dd23f48f3a9406ba394a6d6a6990b11f9daea9103590a8cfb3dc994d44d76b6
6
+ metadata.gz: a01ae2b84eea1af1a46ed86a0727a38418ec88682fddf0e8829e340f39e368adf7e3451e07b05efc423d874cc6bf70910bfc7afe61267db84c6e8ef90d6f1764
7
+ data.tar.gz: 280adedbab88143432826be99f65e17673f9bc0012ddb4c89392b32c6dd2c6ec9ce71a68bf1f79dce2347a631afc5d0f3804092ddbd2d6ef6906fb2af6142534
@@ -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,17 +1,24 @@
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
- gem.email = ['nunemaker@gmail.com']
12
- gem.summary = 'ActiveRecord adapter for Flipper'
18
+ gem.email = 'support@flippercloud.io'
19
+ gem.summary = 'ActiveRecord feature flag adapter for Flipper'
13
20
  gem.license = 'MIT'
14
- gem.homepage = 'https://github.com/jnunemaker/flipper'
21
+ gem.homepage = 'https://www.flippercloud.io/docs/adapters/active-record'
15
22
 
16
23
  extra_files = [
17
24
  'lib/generators/flipper/templates/migration.erb',
@@ -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,42 @@ 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
+ # Abstract base class for internal models
13
+ class Model < ::ActiveRecord::Base
14
+ self.abstract_class = true
15
+ end
14
16
 
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
17
+ # Private: Do not use outside of this adapter.
18
+ class Feature < Model
19
+ self.table_name = [
20
+ Model.table_name_prefix,
21
+ "flipper_features",
22
+ Model.table_name_suffix,
23
+ ].join
24
+
25
+ has_many :gates, foreign_key: "feature_key", primary_key: "key"
26
+
27
+ validates :key, presence: true
28
+ end
29
+
30
+ # Private: Do not use outside of this adapter.
31
+ class Gate < Model
32
+ self.table_name = [
33
+ Model.table_name_prefix,
34
+ "flipper_gates",
35
+ Model.table_name_suffix,
36
+ ].join
23
37
 
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
38
+ validates :feature_key, presence: true
39
+ validates :key, presence: true
40
+ end
31
41
  end
32
42
 
33
- # Public: The name of the adapter.
34
- attr_reader :name
43
+ VALUE_TO_TEXT_WARNING = <<-EOS
44
+ Your database needs migrated to use the latest Flipper features.
45
+ Run `rails generate flipper:update` and `rails db:migrate`.
46
+ EOS
35
47
 
36
48
  # Public: Initialize a new ActiveRecord adapter instance.
37
49
  #
@@ -49,24 +61,27 @@ module Flipper
49
61
  @name = options.fetch(:name, :active_record)
50
62
  @feature_class = options.fetch(:feature_class) { Feature }
51
63
  @gate_class = options.fetch(:gate_class) { Gate }
64
+
65
+ warn VALUE_TO_TEXT_WARNING if value_not_text?
52
66
  end
53
67
 
54
68
  # Public: The set of known features.
55
69
  def features
56
- with_connection(@feature_class) { @feature_class.all.map(&:key).to_set }
70
+ with_connection(@feature_class) { @feature_class.distinct.pluck(:key).to_set }
57
71
  end
58
72
 
59
73
  # Public: Adds a feature to the set of known features.
60
74
  def add(feature)
61
75
  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
76
+ @feature_class.transaction(requires_new: true) do
77
+ begin
78
+ # race condition, but add is only used by enable/disable which happen
79
+ # super rarely, so it shouldn't matter in practice
80
+ unless @feature_class.where(key: feature.key).exists?
81
+ @feature_class.create!(key: feature.key)
69
82
  end
83
+ rescue ::ActiveRecord::RecordNotUnique
84
+ # already added
70
85
  end
71
86
  end
72
87
  end
@@ -117,14 +132,14 @@ module Flipper
117
132
  end
118
133
 
119
134
  def get_all
120
- with_connection(@feature_class) do
135
+ with_connection(@feature_class) do |connection|
121
136
  # query the gates from the db in a single query
122
137
  features = ::Arel::Table.new(@feature_class.table_name.to_sym)
123
138
  gates = ::Arel::Table.new(@gate_class.table_name.to_sym)
124
139
  rows_query = features.join(gates, ::Arel::Nodes::OuterJoin)
125
140
  .on(features[:key].eq(gates[:feature_key]))
126
141
  .project(features[:key].as('feature_key'), gates[:key], gates[:value])
127
- gates = @feature_class.connection.select_rows(rows_query)
142
+ gates = connection.select_rows(rows_query)
128
143
 
129
144
  # group the gates by feature key
130
145
  grouped_gates = gates.inject({}) do |hash, (feature_key, key, value)|
@@ -156,6 +171,8 @@ module Flipper
156
171
  set(feature, gate, thing, clear: true)
157
172
  when :integer
158
173
  set(feature, gate, thing)
174
+ when :json
175
+ set(feature, gate, thing, json: true)
159
176
  when :set
160
177
  enable_multi(feature, gate, thing)
161
178
  else
@@ -178,6 +195,8 @@ module Flipper
178
195
  clear(feature)
179
196
  when :integer
180
197
  set(feature, gate, thing)
198
+ when :json
199
+ delete(feature, gate)
181
200
  when :set
182
201
  with_connection(@gate_class) do
183
202
  @gate_class.where(feature_key: feature.key, key: gate.key, value: thing.value).destroy_all
@@ -198,19 +217,23 @@ module Flipper
198
217
 
199
218
  def set(feature, gate, thing, options = {})
200
219
  clear_feature = options.fetch(:clear, false)
220
+ json_feature = options.fetch(:json, false)
221
+
222
+ raise VALUE_TO_TEXT_WARNING if json_feature && value_not_text?
223
+
201
224
  with_connection(@gate_class) do
202
- @gate_class.transaction do
225
+ @gate_class.transaction(requires_new: true) do
203
226
  clear(feature) if clear_feature
204
- @gate_class.where(feature_key: feature.key, key: gate.key).destroy_all
227
+ delete(feature, gate)
205
228
  begin
206
229
  @gate_class.create! do |g|
207
230
  g.feature_key = feature.key
208
231
  g.key = gate.key
209
- g.value = thing.value.to_s
232
+ g.value = json_feature ? Typecast.to_json(thing.value) : thing.value.to_s
210
233
  end
211
234
  rescue ::ActiveRecord::RecordNotUnique
212
235
  # assume this happened concurrently with the same thing and its fine
213
- # see https://github.com/jnunemaker/flipper/issues/544
236
+ # see https://github.com/flippercloud/flipper/issues/544
214
237
  end
215
238
  end
216
239
  end
@@ -218,18 +241,26 @@ module Flipper
218
241
  nil
219
242
  end
220
243
 
244
+ def delete(feature, gate)
245
+ @gate_class.where(feature_key: feature.key, key: gate.key).destroy_all
246
+ end
247
+
221
248
  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
249
+ with_connection(@gate_class) do |connection|
250
+ begin
251
+ connection.transaction(requires_new: true) do
252
+ @gate_class.create! do |g|
253
+ g.feature_key = feature.key
254
+ g.key = gate.key
255
+ g.value = thing.value.to_s
256
+ end
257
+ end
258
+ rescue ::ActiveRecord::RecordNotUnique
259
+ # already added so move on with life
227
260
  end
228
261
  end
229
262
 
230
263
  nil
231
- rescue ::ActiveRecord::RecordNotUnique
232
- # already added so no need move on with life
233
264
  end
234
265
 
235
266
  def result_for_gates(feature, gates)
@@ -238,13 +269,13 @@ module Flipper
238
269
  feature.gates.each do |gate|
239
270
  result[gate.key] =
240
271
  case gate.data_type
241
- when :boolean
272
+ when :boolean, :integer
242
273
  if row = gates.detect { |key, value| !key.nil? && key.to_sym == gate.key }
243
274
  row.last
244
275
  end
245
- when :integer
276
+ when :json
246
277
  if row = gates.detect { |key, value| !key.nil? && key.to_sym == gate.key }
247
- row.last
278
+ Typecast.from_json(row.last)
248
279
  end
249
280
  when :set
250
281
  gates.select { |key, value| !key.nil? && key.to_sym == gate.key }.map(&:last).to_set
@@ -255,6 +286,15 @@ module Flipper
255
286
  result
256
287
  end
257
288
 
289
+ # Check if value column is text instead of string
290
+ # See https://github.com/flippercloud/flipper/pull/692
291
+ def value_not_text?
292
+ @gate_class.column_for_attribute(:value).type != :text
293
+ rescue ::ActiveRecord::ActiveRecordError => error
294
+ # If the table doesn't exist, the column doesn't exist either
295
+ warn "#{error.message}. You likely need to run `rails g flipper:active_record` and/or `rails db:migrate`."
296
+ end
297
+
258
298
  def with_connection(model = @feature_class, &block)
259
299
  model.connection_pool.with_connection(&block)
260
300
  end
@@ -1,3 +1,13 @@
1
1
  module Flipper
2
- VERSION = '0.28.3'.freeze
2
+ VERSION = '1.3.4'.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: 0.28.3
4
+ version: 1.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-07-21 00:00:00.000000000 Z
10
+ date: 2025-03-03 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: 0.28.3
18
+ version: 1.3.4
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: 0.28.3
25
+ version: 1.3.4
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,10 +42,8 @@ dependencies:
43
42
  version: '4.2'
44
43
  - - "<"
45
44
  - !ruby/object:Gem::Version
46
- version: '8'
47
- description:
48
- email:
49
- - nunemaker@gmail.com
45
+ version: '9'
46
+ email: support@flippercloud.io
50
47
  executables: []
51
48
  extensions: []
52
49
  extra_rdoc_files: []
@@ -57,6 +54,7 @@ files:
57
54
  - examples/active_record/ar_setup.rb
58
55
  - examples/active_record/basic.rb
59
56
  - examples/active_record/cached.rb
57
+ - examples/active_record/group_migration.rb
60
58
  - examples/active_record/internals.rb
61
59
  - flipper-active_record.gemspec
62
60
  - lib/flipper-active_record.rb
@@ -67,12 +65,16 @@ files:
67
65
  - spec/flipper/adapters/active_record_spec.rb
68
66
  - test/adapters/active_record_test.rb
69
67
  - test_rails/generators/flipper/active_record_generator_test.rb
70
- homepage: https://github.com/jnunemaker/flipper
68
+ homepage: https://www.flippercloud.io/docs/adapters/active-record
71
69
  licenses:
72
70
  - MIT
73
71
  metadata:
74
- changelog_uri: https://github.com/jnunemaker/flipper/blob/main/Changelog.md
75
- post_install_message:
72
+ documentation_uri: https://www.flippercloud.io/docs
73
+ homepage_uri: https://www.flippercloud.io
74
+ source_code_uri: https://github.com/flippercloud/flipper
75
+ bug_tracker_uri: https://github.com/flippercloud/flipper/issues
76
+ changelog_uri: https://github.com/flippercloud/flipper/releases/tag/v1.3.4
77
+ funding_uri: https://github.com/sponsors/flippercloud
76
78
  rdoc_options: []
77
79
  require_paths:
78
80
  - lib
@@ -87,10 +89,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
87
89
  - !ruby/object:Gem::Version
88
90
  version: '0'
89
91
  requirements: []
90
- rubygems_version: 3.3.7
91
- signing_key:
92
+ rubygems_version: 3.6.5
92
93
  specification_version: 4
93
- summary: ActiveRecord adapter for Flipper
94
+ summary: ActiveRecord feature flag adapter for Flipper
94
95
  test_files:
95
96
  - spec/flipper/adapters/active_record_spec.rb
96
97
  - test/adapters/active_record_test.rb