bitfields 0.1.0

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.
@@ -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