Skip to content
Browse files

merged kunzmann, expanded tests, works with multiple has_flags per class

  • Loading branch information...
1 parent 36a9fe2 commit d05cbd2e7fe90db480d797744557b2b895d26905 @pboling committed Jul 18, 2011
Showing with 473 additions and 203 deletions.
  1. BIN .DS_Store
  2. +4 −0 .gitignore
  3. +5 −0 Gemfile
  4. +153 −86 README.rdoc
  5. +6 −3 Rakefile
  6. +23 −0 flag_shih_tzu.gemspec
  7. +110 −64 lib/flag_shih_tzu.rb
  8. +3 −0 lib/flag_shih_tzu/version.rb
  9. +1 −1 test/database.yml
  10. +161 −46 test/flag_shih_tzu_test.rb
  11. +4 −2 test/schema.rb
  12. +3 −1 test/test_helper.rb
View
BIN .DS_Store
Binary file not shown.
View
4 .gitignore
@@ -4,3 +4,7 @@ coverage
.idea
.idea/*
.idea/**/*
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
View
5 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
View
239 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:
@@ -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
@@ -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.
+---+---+---+ +---+---+---+
@@ -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
View
9 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
@@ -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'
@@ -28,4 +31,4 @@ namespace :test do
system("rcov -Ilib test/*_test.rb")
system("open coverage/index.html") if PLATFORM['darwin']
end
-end
+end
View
23 flag_shih_tzu.gemspec
@@ -0,0 +1,23 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "flag_shih_tzu/version"
+
+Gem::Specification.new do |s|
+ s.name = "flag_shih_tzu"
+ s.version = FlagShihTzu::VERSION
+ s.platform = Gem::Platform::RUBY
+ s.authors = ["Patryk Peszko", "Sebastian Roebke", "David Anderson", "Tim Payton"]
+ s.homepage = "https://github.com/xing/flag_shih_tzu"
+ s.summary = %q{A rails plugin to store a collection of boolean attributes in a single ActiveRecord column as a bit field}
+ s.description = <<-EODOC
+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
+ActiveRecord object.
+ EODOC
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+end
View
174 lib/flag_shih_tzu.rb
@@ -1,86 +1,112 @@
+require 'active_support/core_ext/class/inheritable_attributes'
+
module FlagShihTzu
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'] # taken from ActiveRecord::ConnectionAdapters::Column
+ DEFAULT_COLUMN_NAME = 'flags'
def self.included(base)
base.extend(ClassMethods)
end
-
+
class IncorrectFlagColumnException < Exception; end
+ class DuplicateFlagColumnException < Exception; end
+ class NoSuchFlagQueryModeException < Exception; end
class NoSuchFlagException < Exception; end
module ClassMethods
def has_flags(*args)
- flag_hash, options = parse_options(*args)
- options = {:named_scopes => true, :column => 'flags', :verbose => false}.update(options)
+ flag_hash, opts = parse_options(*args)
+ opts = {
+ :named_scopes => true,
+ :column => DEFAULT_COLUMN_NAME,
+ :flag_query_mode => :in_list
+ }.update(opts)
+ colmn = opts[:column].to_s
+
+ return unless check_flag_column(colmn)
+
+ # options are stored in a class level hash and apply per-column
+ # the mappings are stored in this class level hash and apply per-column
+ class_eval <<-EVAL
+ unless defined?(HAS_FLAGS_INITIALIZED)
+ (class_attribute :flag_columns, :instance_writer => false)
+ (class_attribute :flag_options, :instance_writer => false)
+ (class_attribute :flag_mapping, :instance_writer => false)
+ end
- class_inheritable_reader :flag_column
- write_inheritable_attribute(:flag_column, options[:column])
+ self.flag_columns ||= []
+ self.flag_columns += ['#{colmn}']
- flag_column = options[:column]
- if check_flag_column(flag_column)
+ self.flag_options = {} if self.flag_options.nil?
+ self.flag_options['#{colmn}'] = opts
- class_inheritable_hash :flag_mapping
# if has_flags is used more than once in a single class, then flag_mapping will already have data in it in successive declarations
- write_inheritable_attribute(:flag_mapping, {}) if flag_mapping.nil?
-
+ self.flag_mapping = {} if self.flag_mapping.nil?
+ #If we already have an instance of the same column in the flag_mapping, then there is a double definition on a column
+ raise DuplicateFlagColumnException unless self.flag_mapping['#{colmn}'].nil?
# initialize flag_mapping for this column
- flag_mapping[flag_column] ||= {}
+ self.flag_mapping['#{colmn}'] ||= {}
- flag_hash.each do |flag_key, flag_name|
- raise ArgumentError, "has_flags: flag keys should be positive integers, and #{flag_key} is not" unless is_valid_flag_key(flag_key)
- raise ArgumentError, "has_flags: flag names should be symbols, and #{flag_name} is not" unless is_valid_flag_name(flag_name)
- raise ArgumentError, "has_flags: flag name #{flag_name} already defined, please choose different name" if method_defined?(flag_name)
+ EVAL
- flag_mapping[flag_column][flag_name] = 1 << (flag_key - 1)
+ flag_hash.each do |flag_key, flag_name|
+ raise ArgumentError, "has_flags: flag keys should be positive integers, and #{flag_key} is not" unless is_valid_flag_key(flag_key)
+ raise ArgumentError, "has_flags: flag names should be symbols, and #{flag_name} is not" unless is_valid_flag_name(flag_name)
+ next if flag_mapping[colmn][flag_name] & (1 << (flag_key - 1)) # next if already methods defined by flag_shih_tzu
+ raise ArgumentError, "has_flags: flag name #{flag_name} already defined, please choose different name" if method_defined?(flag_name)
- class_eval <<-EVAL
- def #{flag_name}
- flag_enabled?(:#{flag_name}, '#{flag_column}')
- end
+ flag_mapping[colmn][flag_name] = 1 << (flag_key - 1)
- def #{flag_name}?
- flag_enabled?(:#{flag_name}, '#{flag_column}')
- end
+ # HAS_FLAGS_INITIALIZED: We don't want to initialize has_flags twice for the same class:
+ class_eval <<-EVAL
+ HAS_FLAGS_INITIALIZED = true unless defined?(HAS_FLAGS_INITIALIZED)
- def #{flag_name}=(value)
- FlagShihTzu::TRUE_VALUES.include?(value) ? enable_flag(:#{flag_name}, '#{flag_column}') : disable_flag(:#{flag_name}, '#{flag_column}')
- end
+ def #{flag_name}
+ flag_enabled?(:#{flag_name}, '#{colmn}')
+ end
- def self.#{flag_name}_condition
- sql_condition_for_flag(:#{flag_name}, '#{flag_column}', true)
- end
+ def #{flag_name}?
+ flag_enabled?(:#{flag_name}, '#{colmn}')
+ end
- def self.not_#{flag_name}_condition
- sql_condition_for_flag(:#{flag_name}, '#{flag_column}', false)
- end
+ def #{flag_name}=(value)
+ FlagShihTzu::TRUE_VALUES.include?(value) ? enable_flag(:#{flag_name}, '#{colmn}') : disable_flag(:#{flag_name}, '#{colmn}')
+ end
- def self.set_#{flag_name}_sql
- sql_set_for_flag(:#{flag_name}, '#{flag_column}', true)
- end
+ def self.#{flag_name}_condition(options = {})
+ sql_condition_for_flag(:#{flag_name}, '#{colmn}', true, options[:table_alias] || self.table_name)
+ end
- def self.unset_#{flag_name}_sql
- sql_set_for_flag(:#{flag_name}, '#{flag_column}', false)
- end
- EVAL
+ def self.not_#{flag_name}_condition
+ sql_condition_for_flag(:#{flag_name}, '#{colmn}', false)
+ end
+
+ def self.set_#{flag_name}_sql
+ sql_set_for_flag(:#{flag_name}, '#{colmn}', true)
+ end
- if respond_to?(:named_scope) && options[:named_scopes]
- class_eval <<-EVAL
- named_scope :#{flag_name}, lambda { { :conditions => #{flag_name}_condition } }
- named_scope :not_#{flag_name}, lambda { { :conditions => not_#{flag_name}_condition } }
- EVAL
+ def self.unset_#{flag_name}_sql
+ sql_set_for_flag(:#{flag_name}, '#{colmn}', false)
end
+ EVAL
+
+ # Define the named scopes if the user wants them and AR supports it
+ if flag_options[colmn][:named_scopes] && respond_to?(named_scope_method)
+ class_eval <<-EVAL
+ #{named_scope_method} :#{flag_name}, lambda { { :conditions => #{flag_name}_condition } }
+ #{named_scope_method} :not_#{flag_name}, lambda { { :conditions => not_#{flag_name}_condition } }
+ EVAL
end
end
end
def check_flag(flag, colmn)
raise ArgumentError, "Column name '#{colmn}' for flag '#{flag}' is not a string" unless colmn.is_a?(String)
- raise ArgumentError, "Invalid flag '#{flag}'" if flag_mapping[colmn].nil? || !flag_mapping[colmn].include?(flag)
+ raise ArgumentError, "Invalid flag '#{flag}'" if self.flag_mapping[colmn].nil? || !self.flag_mapping[colmn].include?(flag)
end
-
private
-
+
def parse_options(*args)
options = args.shift
if args.size >= 1
@@ -97,54 +123,73 @@ def parse_options(*args)
def check_flag_column(colmn, custom_table_name = self.table_name)
# If you aren't using ActiveRecord (eg. you are outside rails) then do not fail here
# If you are using ActiveRecord then you only want to check for the table if the table exists so it won't fail pre-migration
- has_ar = defined?(ActiveRecord) && self.is_a?(ActiveRecord::Base) && ActiveRecord::Base.connected?
+ has_ar = !!defined?(ActiveRecord) && self.respond_to?(:descends_from_active_record?)
# Supposedly Rails 2.3 takes care of this, but this precaution is needed for backwards compatibility
has_table = has_ar ? ActiveRecord::Base.connection.tables.include?(custom_table_name) : true
-
+ logger.warn("Error: Table '#{custom_table_name}' doesn't exist") and return false unless has_table
if has_table
- col = columns.select {|column| column.name == colmn }.first
+ found_column = columns.find {|column| column.name == colmn}
#If you have not yet run the migration that adds the 'flags' column then we don't want to fail, because we need to be able to run the migration
#If the column is there but is of the wrong type, then we must fail, because flag_shih_tzu will not work
- case col
+ case found_column
when nil then puts "Error: Column '#{colmn}' doesn't exist on table '#{custom_table_name}'. Did you forget to run migrations?" and return false
- else raise IncorrectFlagColumnException.new("Table '#{custom_table_name}' must have an integer column named '#{colmn}' in order to use FlagShihTzu") and return false unless col.type == :integer
+ else raise IncorrectFlagColumnException.new("Table '#{custom_table_name}' must have an integer column named '#{colmn}' in order to use FlagShihTzu") and return false unless found_column.type == :integer
end
- elsif has_ar && !has_table
- puts "Error: Table '#{custom_table_name}' doesn't exist" and return false
else
#ActiveRecord gem probably hasn't loaded yet?
end
- return true
+ true
end
- def sql_condition_for_flag(flag, colmn, enabled = true, custom_table_name = self.table_name)
+ def sql_condition_for_flag(flag, colmn, enabled = true, table_name = self.table_name)
check_flag(flag, colmn)
- "(#{custom_table_name}.#{colmn} & #{flag_mapping[colmn][flag]} = #{enabled ? flag_mapping[colmn][flag] : 0})"
+ if flag_options[colmn][:flag_query_mode] == :bit_operator
+ # use & bit operator directly in the SQL query.
+ # This has the drawback of not using an index on the flags colum.
+ "(#{table_name}.#{colmn} & #{flag_mapping[colmn][flag]} = #{enabled ? flag_mapping[colmn][flag] : 0})"
+ elsif flag_options[colmn][:flag_query_mode] == :in_list
+ # use IN() operator in the SQL query.
+ # This has the drawback of becoming a big query when you have lots of flags.
+ neg = enabled ? "" : "not "
+ "(#{table_name}.#{colmn} #{neg}in (#{sql_in_for_flag(flag, colmn).join(',')}))"
+ else
+ raise NoSuchFlagQueryModeException
+ end
end
- def sql_set_for_flag(flag, colmn, enabled = true, custom_table_name = self.table_name)
- check_flag(flag, colmn)
-
- "#{custom_table_name}.#{colmn} = #{custom_table_name}.#{colmn} #{enabled ? "| " : "& ~" }#{flag_mapping[colmn][flag]}"
+ # returns an array of integers suitable for a SQL IN statement.
+ def sql_in_for_flag(flag, colmn)
+ val = flag_mapping[colmn][flag]
+ num = 2 ** flag_mapping[flag_options[colmn][:column]].length
+ (1..num).select {|i| i & val == val}
end
-
+
def is_valid_flag_key(flag_key)
flag_key > 0 && flag_key == flag_key.to_i
end
def is_valid_flag_name(flag_name)
flag_name.is_a?(Symbol)
end
+
+ # Returns the correct method to create a named scope.
+ # Use to prevent deprecation notices on Rails 3 when using +named_scope+ instead of +scope+.
+ def named_scope_method
+ # Can't use respond_to because both AR 2 and 3 respond to both +scope+ and +named_scope+.
+ ActiveRecord::VERSION::MAJOR == 2 ? :named_scope : :scope
+ end
end
+ # Performs the bitwise operation so the flag will return +true+.
def enable_flag(flag, colmn = nil)
colmn = determine_flag_colmn_for(flag) if colmn.nil?
self.class.check_flag(flag, colmn)
set_flags(self.flags(colmn) | self.class.flag_mapping[colmn][flag], colmn)
end
+ # Performs the bitwise operation so the flag will return +false+.
def disable_flag(flag, colmn = nil)
colmn = determine_flag_colmn_for(flag) if colmn.nil?
self.class.check_flag(flag, colmn)
@@ -166,7 +211,7 @@ def flag_disabled?(flag, colmn = nil)
!flag_enabled?(flag, colmn)
end
- def flags(colmn = 'flags')
+ def flags(colmn = DEFAULT_COLUMN_NAME)
self[colmn] || 0
end
@@ -181,11 +226,12 @@ def get_bit_for(flag, colmn)
end
def determine_flag_colmn_for(flag)
- return 'flags' if self.class.flag_mapping.nil?
+ return DEFAULT_COLUMN_NAME if self.class.flag_mapping.nil?
self.class.flag_mapping.each_pair do |colmn, mapping|
return colmn if mapping.include?(flag)
end
raise NoSuchFlagException.new("determine_flag_colmn_for: Couldn't determine column for your flags!")
end
end
+
View
3 lib/flag_shih_tzu/version.rb
@@ -0,0 +1,3 @@
+module FlagShihTzu
+ VERSION = "0.1.0.pre"
+end
View
2 test/database.yml
@@ -3,7 +3,7 @@ sqlite3:
database: test/flag_shih_tzu_plugin.sqlite3.db
mysql:
- adapter: mysql
+ adapter: mysql2
host: localhost
username: root
password:
View
207 test/flag_shih_tzu_test.rb
@@ -1,4 +1,4 @@
-require File.dirname(__FILE__) + '/test_helper.rb'
+require File.expand_path(File.dirname(__FILE__) + '/test_helper.rb')
load_schema
class Spaceship < ActiveRecord::Base
@@ -39,10 +39,25 @@ class SpaceshipWith2CustomFlagsColumn < ActiveRecord::Base
has_flags({ 1 => :jeanlucpicard, 2 => :dajanatroj }, :column => 'commanders')
end
+class SpaceshipWithBitOperatorQueryMode < ActiveRecord::Base
+ set_table_name 'spaceships'
+ include FlagShihTzu
+
+ has_flags(1 => :warpdrive, 2 => :shields, :flag_query_mode => :bit_operator)
+end
+
class SpaceCarrier < Spaceship
end
+# table planets is missing intentionally to see if flagshihtzu handles missing tables gracefully
+class Planet < ActiveRecord::Base
+end
+
class FlagShihTzuClassMethodsTest < Test::Unit::TestCase
+
+# def setup
+# Spaceship.destroy_all
+# end
def test_has_flags_should_raise_an_exception_when_flag_key_is_negative
assert_raises ArgumentError do
@@ -72,6 +87,53 @@ class SpaceshipWithAlreadyUsedFlag < ActiveRecord::Base
)
end
end
+
+ def test_has_flags_should_raise_an_exception_when_desired_flag_name_method_already_defined
+ assert_raises ArgumentError do
+ eval(<<-EOF
+ class SpaceshipWithAlreadyUsedMethod < ActiveRecord::Base
+ set_table_name 'spaceships_with_2_custom_flags_column'
+ include FlagShihTzu
+
+ def jeanluckpicard; end
+
+ has_flags({ 1 => :jeanluckpicard }, :column => 'bits')
+ end
+ EOF
+ )
+ end
+ end
+
+
+ def test_has_flags_should_raise_an_exception_when_flag_column_defined_twice
+ assert_raises FlagShihTzu::DuplicateFlagColumnException do
+ eval(<<-EOF
+ class SpaceshipWithDuplicateFlagsColumn < ActiveRecord::Base
+ set_table_name 'spaceships_with_2_custom_flags_column'
+ include FlagShihTzu
+
+ has_flags({ 1 => :warpdrive, 2 => :hyperspace }, :column => 'bits')
+ has_flags({ 1 => :jeanlucpicard, 2 => :dajanatroj }, :column => 'bits')
+ end
+ EOF
+ )
+ end
+ end
+
+ def test_has_flags_should_not_raise_an_exception_when_mulitple_has_flags_definitions_on_different_columns
+ assert_nothing_raised do
+ eval(<<-EOF
+ class SpaceshipWithAlreadyUsedMethodByFlagshitzu < ActiveRecord::Base
+ set_table_name 'spaceships_with_2_custom_flags_column'
+ include FlagShihTzu
+
+ has_flags({ 1 => :jeanluckpicard }, :column => 'bits')
+ has_flags({ 1 => :mangoes }, :column => 'commanders')
+ end
+ EOF
+ )
+ end
+ end
def test_has_flags_should_raise_an_exception_when_flag_name_is_not_a_symbol
assert_raises ArgumentError do
@@ -88,59 +150,89 @@ class SpaceshipWithInvalidFlagName < ActiveRecord::Base
end
def test_should_define_a_sql_condition_method_for_flag_enabled
- assert_equal "(spaceships.flags & 1 = 1)", Spaceship.warpdrive_condition
- assert_equal "(spaceships.flags & 2 = 2)", Spaceship.shields_condition
- assert_equal "(spaceships.flags & 4 = 4)", Spaceship.electrolytes_condition
+ assert_equal "(spaceships.flags in (1,3,5,7))", Spaceship.warpdrive_condition
+ assert_equal "(spaceships.flags in (2,3,6,7))", Spaceship.shields_condition
+ assert_equal "(spaceships.flags in (4,5,6,7))", Spaceship.electrolytes_condition
+ end
+
+ def test_should_accept_a_table_alias_option_for_sql_condition_method
+ assert_equal "(old_spaceships.flags in (1,3,5,7))", Spaceship.warpdrive_condition(:table_alias => 'old_spaceships')
end
def test_should_define_a_sql_condition_method_for_flag_enabled_with_2_colmns
- assert_equal "(spaceships_with_2_custom_flags_column.bits & 1 = 1)", SpaceshipWith2CustomFlagsColumn.warpdrive_condition
- assert_equal "(spaceships_with_2_custom_flags_column.bits & 2 = 2)", SpaceshipWith2CustomFlagsColumn.hyperspace_condition
- assert_equal "(spaceships_with_2_custom_flags_column.commanders & 1 = 1)", SpaceshipWith2CustomFlagsColumn.jeanlucpicard_condition
- assert_equal "(spaceships_with_2_custom_flags_column.commanders & 2 = 2)", SpaceshipWith2CustomFlagsColumn.dajanatroj_condition
+ assert_equal "(spaceships_with_2_custom_flags_column.bits in (1,3))", SpaceshipWith2CustomFlagsColumn.warpdrive_condition
+ assert_equal "(spaceships_with_2_custom_flags_column.bits in (2,3))", SpaceshipWith2CustomFlagsColumn.hyperspace_condition
+ assert_equal "(spaceships_with_2_custom_flags_column.commanders in (1,3))", SpaceshipWith2CustomFlagsColumn.jeanlucpicard_condition
+ assert_equal "(spaceships_with_2_custom_flags_column.commanders in (2,3))", SpaceshipWith2CustomFlagsColumn.dajanatroj_condition
end
def test_should_define_a_sql_condition_method_for_flag_not_enabled
- assert_equal "(spaceships.flags & 1 = 0)", Spaceship.not_warpdrive_condition
- assert_equal "(spaceships.flags & 2 = 0)", Spaceship.not_shields_condition
- assert_equal "(spaceships.flags & 4 = 0)", Spaceship.not_electrolytes_condition
+ assert_equal "(spaceships.flags not in (1,3,5,7))", Spaceship.not_warpdrive_condition
+ assert_equal "(spaceships.flags not in (2,3,6,7))", Spaceship.not_shields_condition
+ assert_equal "(spaceships.flags not in (4,5,6,7))", Spaceship.not_electrolytes_condition
end
def test_should_define_a_sql_condition_method_for_flag_enabled_with_custom_table_name
- assert_equal "(custom_spaceships.flags & 1 = 1)", Spaceship.send( :sql_condition_for_flag, :warpdrive, 'flags', true, 'custom_spaceships')
+ assert_equal "(custom_spaceships.flags in (1,3,5,7))", Spaceship.send( :sql_condition_for_flag, :warpdrive, 'flags', true, 'custom_spaceships')
end
def test_should_define_a_sql_condition_method_for_flag_enabled_with_2_colmns_not_enabled
- assert_equal "(spaceships_with_2_custom_flags_column.bits & 1 = 0)", SpaceshipWith2CustomFlagsColumn.not_warpdrive_condition
- assert_equal "(spaceships_with_2_custom_flags_column.bits & 2 = 0)", SpaceshipWith2CustomFlagsColumn.not_hyperspace_condition
- assert_equal "(spaceships_with_2_custom_flags_column.commanders & 1 = 0)", SpaceshipWith2CustomFlagsColumn.not_jeanlucpicard_condition
- assert_equal "(spaceships_with_2_custom_flags_column.commanders & 2 = 0)", SpaceshipWith2CustomFlagsColumn.not_dajanatroj_condition
+ assert_equal "(spaceships_with_2_custom_flags_column.bits not in (1,3))", SpaceshipWith2CustomFlagsColumn.not_warpdrive_condition
+ assert_equal "(spaceships_with_2_custom_flags_column.bits not in (2,3))", SpaceshipWith2CustomFlagsColumn.not_hyperspace_condition
+ assert_equal "(spaceships_with_2_custom_flags_column.commanders not in (1,3))", SpaceshipWith2CustomFlagsColumn.not_jeanlucpicard_condition
+ assert_equal "(spaceships_with_2_custom_flags_column.commanders not in (2,3))", SpaceshipWith2CustomFlagsColumn.not_dajanatroj_condition
+ end
+
+ def test_should_define_a_sql_condition_method_for_flag_enabled_using_bit_operators
+ assert_equal "(spaceships.flags & 1 = 1)", SpaceshipWithBitOperatorQueryMode.warpdrive_condition
+ assert_equal "(spaceships.flags & 2 = 2)", SpaceshipWithBitOperatorQueryMode.shields_condition
+ end
+
+ def test_should_define_a_sql_condition_method_for_flag_not_enabled_using_bit_operators
+ assert_equal "(spaceships.flags & 1 = 0)", SpaceshipWithBitOperatorQueryMode.not_warpdrive_condition
+ assert_equal "(spaceships.flags & 2 = 0)", SpaceshipWithBitOperatorQueryMode.not_shields_condition
end
def test_should_define_a_named_scope_for_flag_enabled
- assert_equal({ :conditions => "(spaceships.flags & 1 = 1)" }, Spaceship.warpdrive.proxy_options)
- assert_equal({ :conditions => "(spaceships.flags & 2 = 2)" }, Spaceship.shields.proxy_options)
- assert_equal({ :conditions => "(spaceships.flags & 4 = 4)" }, Spaceship.electrolytes.proxy_options)
+ assert_equal(["(spaceships.flags in (1,3,5,7))"], Spaceship.warpdrive.where_values)
+ assert_equal(["(spaceships.flags in (2,3,6,7))"], Spaceship.shields.where_values)
+ assert_equal(["(spaceships.flags in (4,5,6,7))"], Spaceship.electrolytes.where_values)
end
def test_should_define_a_named_scope_for_flag_not_enabled
- assert_equal({ :conditions => "(spaceships.flags & 1 = 0)" }, Spaceship.not_warpdrive.proxy_options)
- assert_equal({ :conditions => "(spaceships.flags & 2 = 0)" }, Spaceship.not_shields.proxy_options)
- assert_equal({ :conditions => "(spaceships.flags & 4 = 0)" }, Spaceship.not_electrolytes.proxy_options)
+ assert_equal(["(spaceships.flags not in (1,3,5,7))"], Spaceship.not_warpdrive.where_values)
+ assert_equal(["(spaceships.flags not in (2,3,6,7))"], Spaceship.not_shields.where_values)
+ assert_equal(["(spaceships.flags not in (4,5,6,7))"], Spaceship.not_electrolytes.where_values)
end
- def test_should_define_a_named_scope_for_flag_enabled_with_2_columns
- assert_equal({ :conditions => "(spaceships_with_2_custom_flags_column.bits & 1 = 1)" }, SpaceshipWith2CustomFlagsColumn.warpdrive.proxy_options)
- assert_equal({ :conditions => "(spaceships_with_2_custom_flags_column.bits & 2 = 2)" }, SpaceshipWith2CustomFlagsColumn.hyperspace.proxy_options)
- assert_equal({ :conditions => "(spaceships_with_2_custom_flags_column.commanders & 1 = 1)" }, SpaceshipWith2CustomFlagsColumn.jeanlucpicard.proxy_options)
- assert_equal({ :conditions => "(spaceships_with_2_custom_flags_column.commanders & 2 = 2)" }, SpaceshipWith2CustomFlagsColumn.dajanatroj.proxy_options)
+ def test_should_define_a_named_scope_for_flag_enabled_with_2_columns_1
+ assert_equal(["(spaceships_with_2_custom_flags_column.bits in (1,3))"], SpaceshipWith2CustomFlagsColumn.warpdrive.where_values)
+ end
+ def test_should_define_a_named_scope_for_flag_enabled_with_2_columns_2
+ assert_equal(["(spaceships_with_2_custom_flags_column.bits in (2,3))"], SpaceshipWith2CustomFlagsColumn.hyperspace.where_values)
+ end
+ def test_should_define_a_named_scope_for_flag_enabled_with_2_columns_3
+ assert_equal(["(spaceships_with_2_custom_flags_column.commanders in (1,3))"], SpaceshipWith2CustomFlagsColumn.jeanlucpicard.where_values)
+ end
+ def test_should_define_a_named_scope_for_flag_enabled_with_2_columns_4
+ assert_equal(["(spaceships_with_2_custom_flags_column.commanders in (2,3))"], SpaceshipWith2CustomFlagsColumn.dajanatroj.where_values)
end
def test_should_define_a_named_scope_for_flag_not_enabled_with_2_columns
- assert_equal({ :conditions => "(spaceships_with_2_custom_flags_column.bits & 1 = 0)" }, SpaceshipWith2CustomFlagsColumn.not_warpdrive.proxy_options)
- assert_equal({ :conditions => "(spaceships_with_2_custom_flags_column.bits & 2 = 0)" }, SpaceshipWith2CustomFlagsColumn.not_hyperspace.proxy_options)
- assert_equal({ :conditions => "(spaceships_with_2_custom_flags_column.commanders & 1 = 0)" }, SpaceshipWith2CustomFlagsColumn.not_jeanlucpicard.proxy_options)
- assert_equal({ :conditions => "(spaceships_with_2_custom_flags_column.commanders & 2 = 0)" }, SpaceshipWith2CustomFlagsColumn.not_dajanatroj.proxy_options)
+ assert_equal(["(spaceships_with_2_custom_flags_column.bits not in (1,3))"], SpaceshipWith2CustomFlagsColumn.not_warpdrive.where_values)
+ assert_equal(["(spaceships_with_2_custom_flags_column.bits not in (2,3))"], SpaceshipWith2CustomFlagsColumn.not_hyperspace.where_values)
+ assert_equal(["(spaceships_with_2_custom_flags_column.commanders not in (1,3))"], SpaceshipWith2CustomFlagsColumn.not_jeanlucpicard.where_values)
+ assert_equal(["(spaceships_with_2_custom_flags_column.commanders not in (2,3))"], SpaceshipWith2CustomFlagsColumn.not_dajanatroj.where_values)
+ end
+
+ def test_should_define_a_named_scope_for_flag_enabled_using_bit_operators
+ assert_equal(["(spaceships.flags & 1 = 1)"], SpaceshipWithBitOperatorQueryMode.warpdrive.where_values)
+ assert_equal(["(spaceships.flags & 2 = 2)"], SpaceshipWithBitOperatorQueryMode.shields.where_values)
+ end
+
+ def test_should_define_a_named_scope_for_flag_not_enabled_using_bit_operators
+ assert_equal(["(spaceships.flags & 1 = 0)"], SpaceshipWithBitOperatorQueryMode.not_warpdrive.where_values)
+ assert_equal(["(spaceships.flags & 2 = 0)"], SpaceshipWithBitOperatorQueryMode.not_shields.where_values)
end
def test_should_return_the_correct_number_of_items_from_a_named_scope
@@ -177,16 +269,25 @@ def test_should_work_with_a_custom_flags_column
spaceship.save!
spaceship.reload
assert_equal 3, spaceship.flags('bits')
- assert_equal "(spaceships_with_custom_flags_column.bits & 1 = 1)", SpaceshipWithCustomFlagsColumn.warpdrive_condition
- assert_equal "(spaceships_with_custom_flags_column.bits & 1 = 0)", SpaceshipWithCustomFlagsColumn.not_warpdrive_condition
- assert_equal "(spaceships_with_custom_flags_column.bits & 2 = 2)", SpaceshipWithCustomFlagsColumn.hyperspace_condition
- assert_equal "(spaceships_with_custom_flags_column.bits & 2 = 0)", SpaceshipWithCustomFlagsColumn.not_hyperspace_condition
- assert_equal({ :conditions => "(spaceships_with_custom_flags_column.bits & 1 = 1)" }, SpaceshipWithCustomFlagsColumn.warpdrive.proxy_options)
- assert_equal({ :conditions => "(spaceships_with_custom_flags_column.bits & 1 = 0)" }, SpaceshipWithCustomFlagsColumn.not_warpdrive.proxy_options)
- assert_equal({ :conditions => "(spaceships_with_custom_flags_column.bits & 2 = 2)" }, SpaceshipWithCustomFlagsColumn.hyperspace.proxy_options)
- assert_equal({ :conditions => "(spaceships_with_custom_flags_column.bits & 2 = 0)" }, SpaceshipWithCustomFlagsColumn.not_hyperspace.proxy_options)
+ assert_equal "(spaceships_with_custom_flags_column.bits in (1,3))", SpaceshipWithCustomFlagsColumn.warpdrive_condition
+ assert_equal "(spaceships_with_custom_flags_column.bits not in (1,3))", SpaceshipWithCustomFlagsColumn.not_warpdrive_condition
+ assert_equal "(spaceships_with_custom_flags_column.bits in (2,3))", SpaceshipWithCustomFlagsColumn.hyperspace_condition
+ assert_equal "(spaceships_with_custom_flags_column.bits not in (2,3))", SpaceshipWithCustomFlagsColumn.not_hyperspace_condition
+ assert_equal(["(spaceships_with_custom_flags_column.bits in (1,3))"], SpaceshipWithCustomFlagsColumn.warpdrive.where_values)
+ assert_equal(["(spaceships_with_custom_flags_column.bits not in (1,3))"], SpaceshipWithCustomFlagsColumn.not_warpdrive.where_values)
+ assert_equal(["(spaceships_with_custom_flags_column.bits in (2,3))"], SpaceshipWithCustomFlagsColumn.hyperspace.where_values)
+ assert_equal(["(spaceships_with_custom_flags_column.bits not in (2,3))"], SpaceshipWithCustomFlagsColumn.not_hyperspace.where_values)
end
-
+
+ def test_should_not_error_out_when_table_is_not_present
+ assert_nothing_raised(ActiveRecord::StatementInvalid) do
+ Planet.class_eval do
+ include FlagShihTzu
+ has_flags(1 => :habitable)
+ end
+ end
+ end
+
end
class FlagShihTzuInstanceMethodsTest < Test::Unit::TestCase
@@ -296,7 +397,26 @@ def test_should_respect_true_values_like_active_record
assert !@spaceship.warpdrive
end
end
-
+
+#This is a key operational difference between pboling branch on kuzmann
+# def test_should_ignore_has_flags_call_if_column_does_not_exist_yet
+# assert_nothing_raised do
+# eval(<<-EOF
+# class SpaceshipWithoutFlagsColumn < ActiveRecord::Base
+# set_table_name 'spaceships_without_flags_column'
+# include FlagShihTzu
+#
+# has_flags 1 => :warpdrive,
+# 2 => :shields,
+# 3 => :electrolytes
+# end
+# EOF
+# )
+# end
+#
+# assert !SpaceshipWithoutFlagsColumn.method_defined?(:warpdrive)
+# end
+
def test_check_flag_column_raises_error_if_column_not_in_list_of_attributes
assert_raises FlagShihTzu::IncorrectFlagColumnException do
@spaceship.class.send(:check_flag_column, 'incorrect_flags_column')
@@ -397,9 +517,4 @@ def test_should_respect_true_values_like_active_record
assert !@spaceship.warpdrive
end
end
-
- def test_should_return_a_sql_set_method_for_flag
- assert_equal "spaceships.flags = spaceships.flags | 1", Spaceship.send( :sql_set_for_flag, :warpdrive, 'flags', true)
- assert_equal "spaceships.flags = spaceships.flags & ~1", Spaceship.send( :sql_set_for_flag, :warpdrive, 'flags', false)
- end
end
View
6 test/schema.rb
@@ -14,8 +14,10 @@
t.integer :commanders, :null => false, :default => 0
end
- create_table :spaceships_with_non_integer_flags_column, :force => true do |t|
- t.string :flags, :null => false, :default => 0
+ create_table :spaceships_without_flags_column, :force => true do |t|
end
+ create_table :spaceships_with_non_integer_column, :force => true do |t|
+ t.string :flags, :null => false, :default => 'A string'
+ end
end
View
4 test/test_helper.rb
@@ -7,9 +7,11 @@
require 'test/unit'
require 'yaml'
+require 'logger'
require 'rubygems'
+gem 'activerecord', '~> 3.0'
require 'active_record'
-require 'flag_shih_tzu'
+require 'flag_shih_tzu'
def load_schema
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))

0 comments on commit d05cbd2

Please sign in to comment.
Something went wrong with that request. Please try again.