Skip to content

Commit

Permalink
merged kunzmann, expanded tests, works with multiple has_flags per class
Browse files Browse the repository at this point in the history
  • Loading branch information
pboling committed Jul 18, 2011
1 parent 36a9fe2 commit d05cbd2
Show file tree
Hide file tree
Showing 12 changed files with 473 additions and 203 deletions.
Binary file added .DS_Store
Binary file not shown.
4 changes: 4 additions & 0 deletions .gitignore
Expand Up @@ -4,3 +4,7 @@ coverage
.idea
.idea/*
.idea/**/*
*.gem
.bundle
Gemfile.lock
pkg/*
5 changes: 5 additions & 0 deletions Gemfile
@@ -0,0 +1,5 @@
source "http://rubygems.org"

gem 'activesupport', '>3.0.0'
# Specify your gem's dependencies in flag_shih_tzu.gemspec
gemspec
239 changes: 153 additions & 86 deletions README.rdoc
@@ -1,22 +1,22 @@
=FlagShihTzu

A rails plugin to store a collection of boolean attributes in a single
A rails plugin to store a collection of boolean attributes in a single
ActiveRecord column as a bit field.

http://github.com/xing/flag_shih_tzu

This plugin lets you use a single integer column in an ActiveRecord model
to store a collection of boolean attributes (flags). Each flag can be used
almost in the same way you would use any boolean attribute on an
to store a collection of boolean attributes (flags). Each flag can be used
almost in the same way you would use any boolean attribute on an
ActiveRecord object.

The benefits:
* No migrations needed for new boolean attributes. This helps a lot
* No migrations needed for new boolean attributes. This helps a lot
if you have very large db-tables where you want to avoid ALTER TABLE whenever
possible.
* Only the one integer column needs to be indexed.

Using FlagShihTzu, you can add new boolean attributes whenever you want,
Using FlagShihTzu, you can add new boolean attributes whenever you want,
without needing any migration. Just add a new flag to the +has_flags+ call.

And just in case you are wondering what "Shih Tzu" means:
Expand All @@ -25,20 +25,32 @@ http://en.wikipedia.org/wiki/Shih_Tzu

==Prerequisites

FlagShihTzu assumes that your ActiveRecord model already has an integer field
to store the flags, which should be defined to not allow NULL values and
should have a default value of 0 (which means all flags are initially set to
FlagShihTzu assumes that your ActiveRecord model already has an integer field
to store the flags, which should be defined to not allow NULL values and
should have a default value of 0 (which means all flags are initially set to
false).

The plugin has been tested with Rails versions from 2.1 to 2.3 and MySQL,
PostgreSQL and SQLite3 databases.
The plugin has been tested with Rails versions from 2.1 to 3.0 and MySQL,
PostgreSQL and SQLite3 databases. It has been tested with ruby 1.9.2 (with
Rails 3 only).


==Installation

===As plugin (Rails 2.x, Rails 3)

cd path/to/your/rails-project
./script/plugin install git://github.com/xing/flag_shih_tzu.git

===As gem (Rails 3)

Add following line to your Gemfile:

gem 'flag_shih_tzu', '= 0.1.0.pre'

Make sure to install gem with bundler:

bundle install

==Usage

Expand All @@ -50,24 +62,93 @@ PostgreSQL and SQLite3 databases.
has_flags 1 => :warpdrive,
2 => :shields,
3 => :electrolytes

end

+has_flags+ takes a hash. The keys must be positive integers and represent
the position of the bit being used to enable or disable the flag.
+has_flags+ takes a hash. The keys must be positive integers and represent
the position of the bit being used to enable or disable the flag.
<b>The keys must not be changed once in use, or you will get wrong results.</b>
That is why the plugin forces you to set them explicitly.
The values are symbols for the flags being created.


===Using a custom column name

The default column name to store the flags is 'flags', but you can provide a
custom column name using the <tt>:column</tt> option. This allows you to use
different columns for separate flags:

has_flags 1 => :warpdrive,
2 => :shields,
3 => :electrolytes,
:column => 'features'

has_flags 1 => :spock,
2 => :scott,
3 => :kirk,
:column => 'crew'


===Generated instance methods

Calling +has_flags+ as shown above creates the following instance methods
on Spaceship:

Spaceship#warpdrive
Spaceship#warpdrive?
Spaceship#warpdrive=
Spaceship#shields
Spaceship#shields?
Spaceship#shields=
Spaceship#electrolytes
Spaceship#electrolytes?
Spaceship#electrolytes=


===Generated named scopes

The following named scopes become available:

Spaceship.warpdrive # :conditions => "(spaceships.flags in (1,3,5,7))"
Spaceship.not_warpdrive # :conditions => "(spaceships.flags not in (1,3,5,7))"
Spaceship.shields # :conditions => "(spaceships.flags in (2,3,6,7))"
Spaceship.not_shields # :conditions => "(spaceships.flags not in (2,3,6,7))"
Spaceship.electrolytes # :conditions => "(spaceships.flags in (4,5,6,7))"
Spaceship.not_electrolytes # :conditions => "(spaceships.flags not in (4,5,6,7))"

If you do not want the named scopes to be defined, set the
<tt>:named_scopes</tt> option to false when calling +has_flags+:

has_flags 1 => :warpdrive, 2 => :shields, 3 => :electrolytes, :named_scopes => false

In a Rails 3 application, FlagShihTzu will use <tt>scope</tt> internally to generate
the scopes. The option on has_flags is still named <tt>:named_scopes</tt> however.


===Examples for using the generated methods

enterprise = Spaceship.new
enterprise.warpdrive = true
enterprise.shields = true
enterprise.electrolytes = false
enterprise.save

if enterprise.shields?
...
end

Spaceship.warpdrive.find(:all)
Spaceship.not_electrolytes.count
...


===How it stores the values

As said, FlagShihTzu uses a single integer column to store the values for all
the defined flags as a bit field.

The bit position of a flag corresponds to the given key.

This way, we can use bit operators on the stored integer value to set, unset
This way, we can use bit operators on the stored integer value to set, unset
and check individual flags.

+---+---+---+ +---+---+---+
Expand All @@ -94,133 +175,119 @@ and check individual flags.
+---+---+---+ +---+---+---+
| 1 | 1 | 0 | = 4 + 2 = 6 | 1 | 0 | 1 | = 4 + 1 = 5
+---+---+---+ +---+---+---+

Read more about bit fields here: http://en.wikipedia.org/wiki/Bit_field


===Using a custom column name
===Support for manually building conditions

The default column name to store the flags is 'flags', but you can provide a
custom column name using the <tt>:column</tt> option:
The following class methods may support you when manually building
ActiveRecord conditions:

has_flags(1 => :warpdrive, :column => 'bits')
Spaceship.warpdrive_condition # "(spaceships.flags in (1,3,5,7))"
Spaceship.not_warpdrive_condition # "(spaceships.flags not in (1,3,5,7))"
Spaceship.shields_condition # "(spaceships.flags in (2,3,6,7))"
Spaceship.not_shields_condition # "(spaceships.flags not in (2,3,6,7))"
Spaceship.electrolytes_condition # "(spaceships.flags in (4,5,6,7))"
Spaceship.not_electrolytes_condition # "(spaceships.flags not in (4,5,6,7))"

These methods also accept a :table_alias option that can be used when
generating SQL that references the same table more than once:

Spaceship.shields_condition(:table_alias => 'evil_spaceships') # "(evil_spaceships.flags in (2,3,6,7))"

===Generated instance methods

Calling +has_flags+ as shown above creates the following instance methods
on Spaceship:
===Choosing a query mode

Spaceship#warpdrive
Spaceship#warpdrive?
Spaceship#warpdrive=
Spaceship#shields
Spaceship#shields?
Spaceship#shields=
Spaceship#electrolytes
Spaceship#electrolytes?
Spaceship#electrolytes=
While the default way of building the SQL conditions uses an IN() list
(as shown above), this approach will not work well for a high number of flags,
as the value list for IN() grows.

For MySQL, depending on your MySQL settings, this can even hit the
'max_allowed_packet' limit with the generated query.

===Generated named scopes
In this case, consider changing the flag query mode to <tt>:bit_operator</tt>
instead of <tt>:in_list</tt>, like so:

The following named scopes become available:
has_flags 1 => :warpdrive,
2 => :shields,
:flag_query_mode => :bit_operator

This will modify the generated condition and named_scope methods to use bit
operators in the SQL instead of an IN() list:

Spaceship.warpdrive_condition # "(spaceships.flags & 1 = 1)",
Spaceship.not_warpdrive_condition # "(spaceships.flags & 1 = 0)",
Spaceship.shields_condition # "(spaceships.flags & 2 = 2)",
Spaceship.not_shields_condition # "(spaceships.flags & 2 = 0)",

Spaceship.warpdrive # :conditions => "(spaceships.flags & 1 = 1)"
Spaceship.not_warpdrive # :conditions => "(spaceships.flags & 1 = 0)"
Spaceship.shields # :conditions => "(spaceships.flags & 2 = 2)"
Spaceship.not_shields # :conditions => "(spaceships.flags & 2 = 0)"
Spaceship.electrolytes # :conditions => "(spaceships.flags & 4 = 4)"
Spaceship.not_electrolytes # :conditions => "(spaceships.flags & 4 = 0)"

If you do not want the named scopes to be defined, set the
<tt>:named_scopes</tt> option to false when calling +has_flags+:

has_flags(1 => :warpdrive, 2 => :shields, 3 => :electrolytes, :named_scopes => false)


===Support for manually building conditions

The following class methods may support you when manually building
ActiveRecord conditions:

Spaceship.warpdrive_condition # "(spaceships.flags & 1 = 1)"
Spaceship.not_warpdrive_condition # "(spaceships.flags & 1 = 0)"
Spaceship.shields_condition # "(spaceships.flags & 2 = 2)"
Spaceship.not_shields_condition # "(spaceships.flags & 2 = 0)"
Spaceship.electrolytes_condition # "(spaceships.flags & 4 = 4)"
Spaceship.not_electrolytes_condition # "(spaceships.flags & 4 = 0)"


===Example code

enterprise = Spaceship.new
enterprise.warpdrive = true
enterprise.shields = true
enterprise.electrolytes = false
enterprise.save

if enterprise.shields?
...
end

Spaceship.warpdrive.find(:all)
Spaceship.not_electrolytes.count
...
The drawback is that due to the bit operator, this query can not use an index
on the flags column.


==Running the plugin tests

1. (Rails 3 only) Add <tt>mysql2</tt>, <tt>pg</tt> and <tt>sqlite3</tt> gems to your Gemfile.
1. Install flag_shih_tzu as plugin inside working Rails application.
1. Modify <tt>test/database.yml</tt> to fit your test environment.
2. If needed, create the test database you configured in <tt>test/database.yml</tt>.

Then you can run
DB=mysql|postgres|sqlite3 rake test:plugins PLUGIN=flag_shih_tzu
Then you can run

DB=mysql|postgres|sqlite3 rake test:plugins PLUGIN=flag_shih_tzu

from your Rails project root or
DB=mysql|postgres|sqlite3 rake

DB=mysql|postgres|sqlite3 rake

from <tt>vendor/plugins/flag_shih_tzu</tt>.


==Authors

{Patryk Peszko}[http://github.com/ppeszko],
{Sebastian Roebke}[http://github.com/boosty],
{David Anderson}[http://github.com/alpinegizmo]
{Patryk Peszko}[http://github.com/ppeszko],
{Sebastian Roebke}[http://github.com/boosty],
{David Anderson}[http://github.com/alpinegizmo]
and {Tim Payton}[http://github.com/dizzy42]

Please find out more about our work in our
Please find out more about our work in our
{tech blog}[http://blog.xing.com/category/english/tech-blog].


==Contributors

{TobiTobes}[http://github.com/rngtng],
{TobiTobes}[http://github.com/rngtng],
{Martin Stannard}[http://github.com/martinstannard],
{Ladislav Martincik}[http://github.com/lacomartincik],
{Peter Boling}[http://github.com/pboling],
{Thorsten Boettger}[http://github.com/alto]
{Daniel Jagszent}[http://github.com/d--j],
{Thorsten Boettger}[http://github.com/alto],
{Darren Torpey}[http://github.com/darrentorpey],
{Joost Baaij}[http://github.com/tilsammans] and
{Musy Bite}[http://github.com/musybite]


==License

The MIT License

Copyright (c) 2009 {XING AG}[http://www.xing.com/]

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
Expand Down
9 changes: 6 additions & 3 deletions Rakefile
@@ -1,6 +1,9 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
require 'rdoc/task'

require 'bundler'
Bundler::GemHelper.install_tasks

desc 'Default: run unit tests.'
task :default => :test
Expand All @@ -13,7 +16,7 @@ Rake::TestTask.new(:test) do |t|
end

desc 'Generate documentation for the flag_shih_tzu plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
RDoc::Task.new do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'FlagShihTzu'
rdoc.options << '--line-numbers' << '--inline-source'
Expand All @@ -28,4 +31,4 @@ namespace :test do
system("rcov -Ilib test/*_test.rb")
system("open coverage/index.html") if PLATFORM['darwin']
end
end
end

0 comments on commit d05cbd2

Please sign in to comment.