bitfields 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,45 @@
1
+ Save migrations and columns by storing multiple booleans in a single integer.
2
+ e.g. 3 = 1->true 2->true 4->false, 4 = 1->false 2->false 4->true, 5 = 1->true 2->false 4->true
3
+
4
+ class User < ActiveRecord::Base
5
+ include Bitfields
6
+ bitfield :my_bits, 1 => :seller, 2 => :insane, 4 => :stupid
7
+ end
8
+
9
+ user = User.new(:seller => true, :insane => true)
10
+ user.seller == true
11
+ user.stupid? == false
12
+ user.my_bits == 9
13
+
14
+ - records changes `user.chamges == {:seller => [false, true]}`
15
+ - adds scopes `User.seller.stupid.first` (deactivate with `bitfield ..., :scopes => false`)
16
+ - builds sql `User.bitfield_sql(:insane => true, :stupid => false) == 'users.my_bits IN (2, 3)'` (2 and 1+2)
17
+ - builds not-index-using sql with `bitfield ... ,:query_mode => :bit_operator` and `User.bitfield_sql(:insane => true, :stupid => false) == '(users.my_bits & 3) = 1'`, always slower than IN() sql, since it will not use an existing index (tested for up to 64 values)
18
+ - builds update sql `User.set_bitfield_sql(:insane => true, :stupid => false) == 'my_bits = (my_bits | 6) - 4'`
19
+ - gives access to bits `User.bitfields[:my_bits][:stupid] == 4`
20
+
21
+ Install
22
+ =======
23
+ As Gem: ` sudo gem install bitfields `
24
+ Or as Rails plugin: ` script/plugins install git://github.com/grosser/bitfields.git `
25
+
26
+ ### Migration
27
+ ALWAYS set a default, bitfield queries will not work for NULL
28
+ t.integer :my_bits, :default => 0, :null => false
29
+ OR
30
+ add_column :users, :my_bits, :integer, :default => 0, :null => false
31
+
32
+ Usage
33
+ =====
34
+
35
+ # update all users
36
+ User.seller.not_stupid.update_all(User.set_bitfield_sql(:seller => true, :insane => true))
37
+
38
+ # delete the shop when a user is no longer a seller
39
+ before_save :delete_shop, :if => lambda{|u| u.changes['seller'] == [true, false]}
40
+
41
+ Author
42
+ ======
43
+ [Michael Grosser](http://pragmatig.wordpress.com)
44
+ grosser.michael@gmail.com
45
+ Hereby placed under public domain, do what you want, just do not hold me accountable...
@@ -0,0 +1,19 @@
1
+ task :default => :spec
2
+ require 'spec/rake/spectask'
3
+ Spec::Rake::SpecTask.new {|t| t.spec_opts = ['--color']}
4
+
5
+ begin
6
+ require 'jeweler'
7
+ project_name = 'bitfields'
8
+ Jeweler::Tasks.new do |gem|
9
+ gem.name = project_name
10
+ gem.summary = "Save migrations and columns by storing multiple booleans in a single integer."
11
+ gem.email = "grosser.michael@gmail.com"
12
+ gem.homepage = "http://github.com/grosser/#{project_name}"
13
+ gem.authors = ["Michael Grosser"]
14
+ end
15
+
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install jeweler"
19
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,139 @@
1
+ require 'active_support'
2
+
3
+ module Bitfields
4
+ VERSION = File.read( File.join(File.dirname(__FILE__),'..','VERSION') ).strip
5
+ TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'] # taken from ActiveRecord::ConnectionAdapters::Column
6
+
7
+ def self.included(base)
8
+ base.class_inheritable_accessor :bitfields, :bitfield_options
9
+ base.extend Bitfields::ClassMethods
10
+ end
11
+
12
+ def self.extract_bits(options)
13
+ bitfields = {}
14
+ options.keys.select{|key| key.is_a?(Numeric) }.each do |bit|
15
+ bit_name = options.delete(bit).to_sym
16
+ bitfields[bit_name] = bit
17
+ end
18
+ bitfields
19
+ end
20
+
21
+ module ClassMethods
22
+ def bitfield(column, options)
23
+ # prepare ...
24
+ column = column.to_sym
25
+ options = options.dup # since we will modify them...
26
+
27
+ # extract options
28
+ self.bitfields ||= {}
29
+ self.bitfield_options ||= {}
30
+ bitfields[column] = Bitfields.extract_bits(options)
31
+ bitfield_options[column] = options
32
+
33
+ # add instance methods and scopes
34
+ bitfields[column].keys.each do |bit_name|
35
+ define_method(bit_name){ bitfield_value(bit_name) }
36
+ define_method("#{bit_name}?"){ bitfield_value(bit_name) }
37
+ define_method("#{bit_name}="){|value| set_bitfield_value(bit_name, value) }
38
+ if options[:scopes] != false
39
+ scoping_method = (respond_to?(:scope) ? :scope : :named_scope) # AR 3.0+ uses scope
40
+ send scoping_method, bit_name, :conditions => bitfield_sql(bit_name => true)
41
+ send scoping_method, "not_#{bit_name}", :conditions => bitfield_sql(bit_name => false)
42
+ end
43
+ end
44
+
45
+ include Bitfields::InstanceMethods
46
+ end
47
+
48
+ def bitfield_column(bit_name)
49
+ bitfields.detect{|c, bits| bits.keys.include?(bit_name.to_sym) }.first
50
+ end
51
+
52
+ def bitfield_sql(bit_values)
53
+ bits = group_bits_by_column(bit_values)
54
+ bits.map{|column, bit_values| bitfield_sql_by_column(column, bit_values) } * ' AND '
55
+ end
56
+
57
+ def set_bitfield_sql(bit_values)
58
+ columns = group_bits_by_column(bit_values)
59
+ columns.map{|column, bit_values| set_bitfield_sql_by_column(column, bit_values) } * ', '
60
+ end
61
+
62
+ private
63
+
64
+ def bitfield_sql_by_column(column, bit_values)
65
+ mode = (bitfield_options[column][:query_mode] || :in_list)
66
+ case mode
67
+ when :in_list then
68
+ max = (bitfields[column].values.max * 2) - 1
69
+ bits = (0..max).to_a # all possible bits
70
+ bit_values.each do |bit_name, value|
71
+ bit = bitfields[column][bit_name]
72
+ # reject values with: bit off for true, bit on for false
73
+ bits.reject!{|i| i & bit == (value ? 0 : bit) }
74
+ end
75
+ "#{table_name}.#{column} IN (#{bits * ','})"
76
+ when :bit_operator
77
+ on, off = bit_values_to_on_off(column, bit_values)
78
+ "(#{table_name}.#{column} & #{on+off}) = #{on}"
79
+ else raise("bitfields: unknown query mode #{mode.inspect}")
80
+ end
81
+ end
82
+
83
+ def set_bitfield_sql_by_column(column, bit_values)
84
+ on, off = bit_values_to_on_off(column, bit_values)
85
+ "#{column} = (#{column} | #{on+off}) - #{off}"
86
+ end
87
+
88
+ def group_bits_by_column(bit_values)
89
+ columns = {}
90
+ bit_values.each do |bit_name, value|
91
+ column = bitfield_column(bit_name.to_sym)
92
+ columns[column] ||= {}
93
+ columns[column][bit_name.to_sym] = value
94
+ end
95
+ columns
96
+ end
97
+
98
+ def bit_values_to_on_off(column, bit_values)
99
+ on = off = 0
100
+ bit_values.each do |bit_name, value|
101
+ bit = bitfields[column][bit_name]
102
+ value ? on += bit : off += bit
103
+ end
104
+ [on, off]
105
+ end
106
+ end
107
+
108
+ module InstanceMethods
109
+ private
110
+ def bitfield_value(bit_name)
111
+ column, bit, current_value = bitfield_info(bit_name)
112
+ current_value & bit != 0
113
+ end
114
+
115
+ def set_bitfield_value(bit_name, value)
116
+ column, bit, current_value = bitfield_info(bit_name)
117
+ new_value = TRUE_VALUES.include?(value)
118
+ old_value = bitfield_value(bit_name)
119
+ return if new_value == old_value
120
+
121
+ if defined? changed_attributes
122
+ send(:changed_attributes).merge!(bit_name.to_s => old_value)
123
+ end
124
+
125
+ # 8 + 1 == 9 // 8 + 8 == 8 // 1 - 8 == 1 // 8 - 8 == 0
126
+ new_bits = if new_value then current_value | bit else (current_value | bit) - bit end
127
+ send("#{column}=", new_bits)
128
+ end
129
+
130
+ def bitfield_info(bit_name)
131
+ column = self.class.bitfield_column(bit_name)
132
+ [
133
+ column,
134
+ self.class.bitfields[column][bit_name], # bit
135
+ (send(column)||0) # current value
136
+ ]
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,277 @@
1
+ require 'spec/spec_helper'
2
+
3
+ class User < ActiveRecord::Base
4
+ include Bitfields
5
+ bitfield :bits, 1 => :seller, 2 => :insane, 4 => :stupid
6
+ end
7
+
8
+ class UserWithBitfieldOptions < ActiveRecord::Base
9
+ include Bitfields
10
+ bitfield :bits, 1 => :seller, 2 => :insane, 4 => :stupid, :scopes => false
11
+ end
12
+
13
+ class MultiBitUser < ActiveRecord::Base
14
+ set_table_name 'users'
15
+ include Bitfields
16
+ bitfield :bits, 1 => :seller, 2 => :insane, 4 => :stupid
17
+ bitfield :more_bits, 1 => :one, 2 => :two, 4 => :four
18
+ end
19
+
20
+ class UserWithoutScopes < ActiveRecord::Base
21
+ set_table_name 'users'
22
+ include Bitfields
23
+ bitfield :bits, 1 => :seller, 2 => :insane, 4 => :stupid, :scopes => false
24
+ end
25
+
26
+ class InheritedUser < User
27
+ end
28
+
29
+ class OverwrittenUser < User
30
+ bitfield :bits, 1 => :seller_inherited
31
+ end
32
+
33
+ class BitOperatorMode < ActiveRecord::Base
34
+ set_table_name 'users'
35
+ include Bitfields
36
+ bitfield :bits, 1 => :seller, 2 => :insane, :query_mode => :bit_operator
37
+ end
38
+
39
+ describe Bitfields do
40
+ before do
41
+ User.delete_all
42
+ end
43
+
44
+ describe :bitfields do
45
+ it "parses them correctly" do
46
+ User.bitfields.should == {:bits => {:seller => 1, :insane => 2, :stupid => 4}}
47
+ end
48
+ end
49
+
50
+ describe :bitfield_options do
51
+ it "parses them correctly when not set" do
52
+ User.bitfield_options.should == {:bits => {}}
53
+ end
54
+
55
+ it "parses them correctly when set" do
56
+ UserWithBitfieldOptions.bitfield_options.should == {:bits => {:scopes => false}}
57
+ end
58
+ end
59
+
60
+ describe 'attribute accessors' do
61
+ it "has everything on false by default" do
62
+ User.new.seller.should == false
63
+ User.new.seller?.should == false
64
+ end
65
+
66
+ it "is true when set to true" do
67
+ User.new(:seller => true).seller.should == true
68
+ end
69
+
70
+ it "is true when set to truthy" do
71
+ User.new(:seller => 1).seller.should == true
72
+ end
73
+
74
+ it "is false when set to false" do
75
+ User.new(:seller => false).seller.should == false
76
+ end
77
+
78
+ it "is false when set to falsy" do
79
+ User.new(:seller => 'false').seller.should == false
80
+ end
81
+
82
+ it "stays true when set to true twice" do
83
+ u = User.new
84
+ u.seller = true
85
+ u.seller = true
86
+ u.seller.should == true
87
+ u.bits.should == 1
88
+ end
89
+
90
+ it "stays false when set to false twice" do
91
+ u = User.new(:bits => 3)
92
+ u.seller = false
93
+ u.seller = false
94
+ u.seller.should == false
95
+ u.bits.should == 2
96
+ end
97
+
98
+ it "changes the bits when setting to false" do
99
+ user = User.new(:bits => 7)
100
+ user.seller = false
101
+ user.bits.should == 6
102
+ end
103
+
104
+ it "does not get negative when unsetting high bits" do
105
+ user = User.new(:seller => true)
106
+ user.stupid = false
107
+ user.bits.should == 1
108
+ end
109
+
110
+ it "changes the bits when setting to true" do
111
+ user = User.new(:bits => 2)
112
+ user.seller = true
113
+ user.bits.should == 3
114
+ end
115
+
116
+ it "does not get too high when setting high bits" do
117
+ user = User.new(:bits => 7)
118
+ user.seller = true
119
+ user.bits.should == 7
120
+ end
121
+
122
+ describe 'changes' do
123
+ it "has no changes by defaut" do
124
+ User.new.changes.should == {}
125
+ end
126
+
127
+ it "records a change when setting" do
128
+ User.new(:seller => true).changes.should == {'seller' => [false, true], 'bits' => [0,1]}
129
+ end
130
+
131
+ it "records a change when unsetting" do
132
+ u = User.create!(:seller => true)
133
+ u.seller = false
134
+ u.changes.should == {'seller' => [true, false], 'bits' => [1,0]}
135
+ end
136
+
137
+ it "does not track duplicate changes" do
138
+ User.create!(:seller => false).changes.should == {}
139
+ end
140
+ end
141
+ end
142
+
143
+ describe :bitfield_sql do
144
+ it "includes true states" do
145
+ User.bitfield_sql(:insane => true).should == 'users.bits IN (2,3,6,7)' # 2, 1+2, 2+4, 1+2+4
146
+ end
147
+
148
+ it "includes invalid states" do
149
+ User.bitfield_sql(:insane => false).should == 'users.bits IN (0,1,4,5)' # 0, 1, 4, 4+1
150
+ end
151
+
152
+ it "can combine multiple fields" do
153
+ User.bitfield_sql(:seller => true, :insane => true).should == 'users.bits IN (3,7)' # 1+2, 1+2+4
154
+ end
155
+
156
+ it "can combine multiple fields with different values" do
157
+ User.bitfield_sql(:seller => true, :insane => false).should == 'users.bits IN (1,5)' # 1, 1+4
158
+ end
159
+
160
+ it "combines multiple columns into one sql" do
161
+ sql = MultiBitUser.bitfield_sql(:seller => true, :insane => false, :one => true, :four => true)
162
+ sql.should == 'users.bits IN (1,5) AND users.more_bits IN (5,7)' # 1, 1+4 AND 1+4, 1+2+4
163
+ end
164
+
165
+ it "produces working sql" do
166
+ u1 = MultiBitUser.create!(:seller => true, :one => true)
167
+ u2 = MultiBitUser.create!(:seller => true, :one => false)
168
+ u3 = MultiBitUser.create!(:seller => false, :one => false)
169
+ MultiBitUser.all(:conditions => MultiBitUser.bitfield_sql(:seller => true, :one => false)).should == [u2]
170
+ end
171
+
172
+ describe 'with bit operator mode' do
173
+ it "generates bit-operator sql" do
174
+ BitOperatorMode.bitfield_sql(:seller => true).should == '(users.bits & 1) = 1'
175
+ end
176
+
177
+ it "generates sql for each bit" do
178
+ BitOperatorMode.bitfield_sql(:seller => true, :insane => false).should == '(users.bits & 3) = 1'
179
+ end
180
+
181
+ it "generates working sql" do
182
+ u1 = BitOperatorMode.create!(:seller => true, :insane => true)
183
+ u2 = BitOperatorMode.create!(:seller => true, :insane => false)
184
+ u3 = BitOperatorMode.create!(:seller => false, :insane => false)
185
+ BitOperatorMode.all(:conditions => MultiBitUser.bitfield_sql(:seller => true, :insane => false)).should == [u2]
186
+ end
187
+ end
188
+ end
189
+
190
+ describe :set_bitfield_sql do
191
+ it "sets a single bit" do
192
+ User.set_bitfield_sql(:seller => true).should == 'bits = (bits | 1) - 0'
193
+ end
194
+
195
+ it "unsets a single bit" do
196
+ User.set_bitfield_sql(:seller => false).should == 'bits = (bits | 1) - 1'
197
+ end
198
+
199
+ it "sets multiple bits" do
200
+ User.set_bitfield_sql(:seller => true, :insane => true).should == 'bits = (bits | 3) - 0'
201
+ end
202
+
203
+ it "unsets multiple bits" do
204
+ User.set_bitfield_sql(:seller => false, :insane => false).should == 'bits = (bits | 3) - 3'
205
+ end
206
+
207
+ it "sets and unsets in one command" do
208
+ User.set_bitfield_sql(:seller => false, :insane => true).should == 'bits = (bits | 3) - 1'
209
+ end
210
+
211
+ it "sets and unsets for multiple columns in one sql" do
212
+ sql = MultiBitUser.set_bitfield_sql(:seller => false, :insane => true, :one => true, :two => false)
213
+ sql.should == "bits = (bits | 3) - 1, more_bits = (more_bits | 3) - 2"
214
+ end
215
+
216
+ it "produces working sql" do
217
+ u = MultiBitUser.create!(:seller => true, :insane => true, :stupid => false, :one => true, :two => false, :four => false)
218
+ sql = MultiBitUser.set_bitfield_sql(:seller => false, :insane => true, :one => true, :two => false)
219
+ MultiBitUser.update_all(sql)
220
+ u.reload
221
+ u.seller.should == false
222
+ u.insane.should == true
223
+ u.stupid.should == false
224
+ u.one.should == true
225
+ u.two.should == false
226
+ u.four.should == false
227
+ end
228
+ end
229
+
230
+ describe 'named scopes' do
231
+ before do
232
+ @u1 = User.create!(:seller => true, :insane => false)
233
+ @u2 = User.create!(:seller => true, :insane => true)
234
+ end
235
+
236
+ it "creates them when nothing was passed" do
237
+ User.respond_to?(:seller).should == true
238
+ User.respond_to?(:not_seller).should == true
239
+ end
240
+
241
+ it "does not create them when false was passed" do
242
+ UserWithoutScopes.respond_to?(:seller).should == false
243
+ UserWithoutScopes.respond_to?(:not_seller).should == false
244
+ end
245
+
246
+ it "produces working positive scopes" do
247
+ User.insane.seller.to_a.should == [@u2]
248
+ end
249
+
250
+ it "produces working negative scopes" do
251
+ User.not_insane.seller.to_a.should == [@u1]
252
+ end
253
+ end
254
+
255
+ describe 'overwriting' do
256
+ it "does not change base class" do
257
+ OverwrittenUser.bitfields[:bits][:seller_inherited].should_not == nil
258
+ User.bitfields[:bits][:seller_inherited].should == nil
259
+ end
260
+
261
+ it "has inherited methods" do
262
+ User.respond_to?(:seller).should == true
263
+ OverwrittenUser.respond_to?(:seller).should == true
264
+ end
265
+
266
+ it "knows inherited values" do
267
+ pending
268
+ OverwrittenUser.bitfield_column(:seller).should == :bits
269
+ end
270
+ end
271
+
272
+ describe 'inheritance' do
273
+ it "knows inherited values" do
274
+ InheritedUser.bitfield_column(:seller).should == :bits
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,15 @@
1
+ require 'active_record'
2
+
3
+ # connect
4
+ ActiveRecord::Base.establish_connection(
5
+ :adapter => "sqlite3",
6
+ :database => ":memory:"
7
+ )
8
+
9
+ # create tables
10
+ ActiveRecord::Schema.define(:version => 1) do
11
+ create_table :users do |t|
12
+ t.integer :bits, :default => 0, :null => false
13
+ t.integer :more_bits, :default => 0, :null => false
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ $LOAD_PATH << 'lib'
3
+ require 'bitfields'
4
+ require 'spec/database'
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bitfields
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Michael Grosser
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-03-06 00:00:00 +01:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description:
22
+ email: grosser.michael@gmail.com
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files:
28
+ - README.markdown
29
+ files:
30
+ - README.markdown
31
+ - Rakefile
32
+ - VERSION
33
+ - lib/bitfields.rb
34
+ - spec/bitfields_spec.rb
35
+ - spec/database.rb
36
+ - spec/spec_helper.rb
37
+ has_rdoc: true
38
+ homepage: http://github.com/grosser/bitfields
39
+ licenses: []
40
+
41
+ post_install_message:
42
+ rdoc_options:
43
+ - --charset=UTF-8
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ segments:
51
+ - 0
52
+ version: "0"
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.3.6
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: Save migrations and columns by storing multiple booleans in a single integer.
67
+ test_files:
68
+ - spec/spec_helper.rb
69
+ - spec/bitfields_spec.rb
70
+ - spec/database.rb