enum_table 0.0.1

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,2 @@
1
+ /Gemfile.lock
2
+ /*.gem
@@ -0,0 +1,3 @@
1
+ == 0.0.1 2012-11-05
2
+
3
+ * Hi.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) George Ogata
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,136 @@
1
+ ## Enum Table
2
+
3
+ Table-based enumerations for ActiveRecord.
4
+
5
+ ## What?
6
+
7
+ When you have a column that should only take one of a finite set of string
8
+ values (e.g., gender, statuses through some workflow), it's usually best not to
9
+ store these as strings. Many databases, such as MySQL and PostgreSQL have native
10
+ enum types which effectively let you treat these as strings while storing them
11
+ internally using as few bytes as possible. Indeed there are already
12
+ [plugins][enum_column3] that let you use these native enum types.
13
+
14
+ But sometimes this is inadequate.
15
+
16
+ Most obviously, not all databases have a native enum type, notably SQLite.
17
+
18
+ Further, in the case of MySQL, the enum type leaves a lot to be desired in a
19
+ production setting. If you need to do anything to the list of allowed values
20
+ other than adding values to the end of the list, MySQL will rebuild the entire
21
+ table, which can be very slow. Unless you're using something like
22
+ [pt-online-schema-change][pt-osc], it will also lock the table during this
23
+ period, which could be unacceptable for large tables.
24
+
25
+ A common alternative is to simply keep the value-to-id mapping in the
26
+ application, hardcoded in the model. The downside of this is that your database
27
+ is no longer self-documenting: the integers mean nothing without the value
28
+ mapping buried in your application code, making it hard to work with the
29
+ database directly. Another problem is the database cannot enforce any
30
+ [referential integrity][foreigner].
31
+
32
+ This plugin implements a different strategy which solves the above problems -
33
+ each enum is defined by a table with `id` and `value` columns, which defines the
34
+ values and the integers they map to. Altering the values can be done with simple
35
+ DDL statements, which do not require rebuilding any tables.
36
+
37
+ [enum_column3]: https://github.com/taktsoft/enum_column3
38
+ [pt-osc]: http://www.percona.com/doc/percona-toolkit/2.1/pt-online-schema-change.html
39
+ [foreigner]: https://github.com/matthuhiggins/foreigner
40
+
41
+ ## Usage
42
+
43
+ Create your enum tables in migrations. Example:
44
+
45
+ create_enum_table :user_genders do |t|
46
+ t.add :male
47
+ t.add :female
48
+ end
49
+
50
+ Then add the enum ID to your model table:
51
+
52
+ add_column :users, :gender_id, null: false
53
+
54
+ Then in your model:
55
+
56
+ class User < ActiveRecord::Base
57
+ enum :gender
58
+ end
59
+
60
+ Note the convention: for a model `User` with enum `gender`, the column is
61
+ `users.gender_id`, and the enum_table is `user_genders`. You can override these
62
+ with the `:id_name` and `:table` options:
63
+
64
+ enum :gender, id_name: :sex_id, table: :sexes
65
+
66
+ ### Custom columns
67
+
68
+ While the names `id` and `value` are fixed, you can change other attributes of
69
+ the column. For example, the ID has `limit: 1` by default, but you can change
70
+ this if you have a large list of enum values:
71
+
72
+ create_enum_table :user_countries do |t|
73
+ t.id limit: 2
74
+ t.add 'Afghanistan'
75
+ t.add 'Albania'
76
+ # ...
77
+ end
78
+
79
+ Similarly you can customize the `value` column, say if you want to place a
80
+ varchar limit:
81
+
82
+ create_enum_table :user_countries do |t|
83
+ t.value limit: 100
84
+ # ...
85
+ end
86
+
87
+ ### Updating enums
88
+
89
+ To change the list of enums:
90
+
91
+ change_enum_table :user_genders do |t|
92
+ t.add :other
93
+ t.remove :male
94
+ end
95
+
96
+ To drop an enum table:
97
+
98
+ drop_enum_table :user_genders
99
+
100
+ Under the hood, `create_enum_table` and `drop_enum_table` maintain the list of
101
+ enum tables in the `enum_tables` table. This allows the table data to be tracked
102
+ by `db/schema.rb` so it gets copied to your test database.
103
+
104
+ ### Hardcoded mappings
105
+
106
+ If you really want, you can forego the table completely and just hardcode the
107
+ ids and values in your model:
108
+
109
+ enum :genders, table: {male: 1, female: 2}
110
+
111
+ Or since our IDs are 1-based and sequential:
112
+
113
+ enum :genders, table: [:male, :female]
114
+
115
+ Of course, by not using tables, you lose some of the advantages mentioned
116
+ earlier, namely a self-documenting database and referential integrity.
117
+
118
+ ### Values
119
+
120
+ By default, `user.gender` will be either the symbol `:male` or `:female`. If
121
+ you're transitioning from using old-fashioned `varchar`s, however, you may find
122
+ it less disruptive to use strings instead. Do that with the `:type` option:
123
+
124
+ enum :genders, type: :string
125
+
126
+ ## Contributing
127
+
128
+ * [Bug reports](https://github.com/howaboutwe/enum_table/issues)
129
+ * [Source](https://github.com/howaboutwe/enum_table)
130
+ * Patches: Fork on Github, send pull request.
131
+ * Include tests where practical.
132
+ * Leave the version alone, or bump it in a separate commit.
133
+
134
+ ## Copyright
135
+
136
+ Copyright (c) HowAboutWe. See LICENSE for details.
@@ -0,0 +1 @@
1
+ require 'ritual'
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.unshift File.expand_path('lib', File.dirname(__FILE__))
3
+ require 'enum_table/version'
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = 'enum_table'
7
+ gem.version = EnumTable::VERSION
8
+ gem.authors = ['George Ogata']
9
+ gem.email = ['george.ogata@gmail.com']
10
+ gem.summary = "Enumeration tables for ActiveRecord"
11
+ gem.homepage = 'http://github.com/howaboutwe/enum_table'
12
+
13
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
14
+ gem.files = `git ls-files`.split("\n")
15
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+
17
+ gem.add_runtime_dependency 'activerecord', '~> 3.2.0'
18
+ gem.add_development_dependency 'ritual', '~> 0.4.1'
19
+ end
@@ -0,0 +1,11 @@
1
+ module EnumTable
2
+ autoload :VERSION, 'enum_table/version'
3
+ autoload :Record, 'enum_table/record'
4
+ autoload :Reflection, 'enum_table/reflection'
5
+ autoload :SchemaDumper, 'enum_table/schema_dumper'
6
+ autoload :SchemaStatements, 'enum_table/schema_statements'
7
+ end
8
+
9
+ require 'enum_table/railtie' if defined?(Rails)
10
+ ActiveRecord::Base.send :include, EnumTable::Record
11
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.send :include, EnumTable::SchemaStatements
@@ -0,0 +1,13 @@
1
+ module EnumTable
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks do
4
+ namespace :enum_table do
5
+ task :load_schema_dumper do
6
+ require 'enum_table/schema_dumper'
7
+ end
8
+ end
9
+
10
+ Rake::Task['db:schema:dump'].prerequisites << 'enum_table:load_schema_dumper'
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,214 @@
1
+ module EnumTable
2
+ module Record
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :enums
7
+ self.enums = {}
8
+ end
9
+
10
+ module ClassMethods
11
+ def enum(name, options={})
12
+ name = name.to_sym
13
+ reflection = enums[name] ? enums[name].dup : Reflection.new(name)
14
+ [:type, :id_name].each do |key|
15
+ value = options[key] and
16
+ reflection.send "#{key}=", value
17
+ end
18
+ enum_map(name, options).each do |value, id|
19
+ reflection.add_value id, value
20
+ end
21
+ self.enums = enums.merge(name => reflection, name.to_s => reflection)
22
+
23
+ class_eval <<-EOS
24
+ def #{name}
25
+ read_enum(:#{name})
26
+ end
27
+
28
+ def #{name}=(value)
29
+ write_enum(:#{name}, value)
30
+ end
31
+
32
+ def #{name}?
33
+ query_enum(:#{name})
34
+ end
35
+
36
+ def #{name}_changed?
37
+ enum_changed?(:#{name})
38
+ end
39
+
40
+ def #{name}_was
41
+ enum_was(:#{name})
42
+ end
43
+
44
+ def #{name}_change
45
+ enum_change(:#{name})
46
+ end
47
+ EOS
48
+
49
+ reflection
50
+ end
51
+
52
+ def enum_map(name, options)
53
+ case (table = options[:table])
54
+ when Hash
55
+ table
56
+ when Array
57
+ map = {}
58
+ table.each_with_index { |element, i| map[element] = i + 1 }
59
+ map
60
+ when String, Symbol, nil
61
+ map = {}
62
+ table_name = table || "#{self.table_name.singularize}_#{name.to_s.pluralize}"
63
+ connection.execute("SELECT id, value FROM #{table_name}").each do |row|
64
+ map[row[1]] = row[0]
65
+ end
66
+ map
67
+ else
68
+ raise ArgumentError, "invalid table specifier: #{table.inspect}"
69
+ end
70
+ end
71
+
72
+ def reflect_on_enum(name)
73
+ enums[name]
74
+ end
75
+
76
+ def enum_id(name, value)
77
+ reflection = enums[name] or
78
+ raise ArgumentError, "no such enum: #{name}"
79
+ reflection.id(value)
80
+ end
81
+
82
+ # Enables enums for STI types.
83
+ def builtin_inheritance_column # :nodoc:
84
+ # Can this be made less brittle?
85
+ if self == ActiveRecord::Base
86
+ 'type'
87
+ else
88
+ (@builtin_inheritance_column ||= nil) || superclass.builtin_inheritance_column
89
+ end
90
+ end
91
+
92
+ def inheritance_enum # :nodoc:
93
+ @inheritance_enum ||= enums[builtin_inheritance_column.to_sym]
94
+ end
95
+
96
+ def inheritance_column # :nodoc:
97
+ (reflection = inheritance_enum) ? reflection.id_name.to_s : super
98
+ end
99
+
100
+ def sti_name # :nodoc:
101
+ (reflection = inheritance_enum) ? reflection.id(super) : super
102
+ end
103
+
104
+ def find_sti_class(type_name) # :nodoc:
105
+ (reflection = inheritance_enum) ? super(reflection.value(type_name).to_s) : super
106
+ end
107
+
108
+ # Enables .find_by_name(value) for enums.
109
+ def expand_hash_conditions_for_aggregates(attrs) # :nodoc:
110
+ conditions = super
111
+ enums.each do |name, reflection|
112
+ if conditions.key?(name)
113
+ value = conditions.delete(name)
114
+ elsif conditions.key?((string_name = name.to_s))
115
+ value = conditions.delete(string_name)
116
+ else
117
+ next
118
+ end
119
+ if value.is_a?(Array)
120
+ id = value.map { |el| reflection.id(el) }
121
+ else
122
+ id = reflection.id(value)
123
+ end
124
+ conditions[reflection.id_name] = id
125
+ end
126
+ conditions
127
+ end
128
+
129
+ # Enables .where(name: value) for enums.
130
+ def expand_attribute_names_for_aggregates(attribute_names) # :nodoc:
131
+ attribute_names = super
132
+ enums.each do |name, reflection|
133
+ index = attribute_names.index(name) and
134
+ attribute_names[index] = reflection.id_name
135
+ end
136
+ attribute_names
137
+ end
138
+
139
+ # Enables state_machine to set initial values for states. Ick.
140
+ def initialize_attributes(attributes) # :nodoc:
141
+ attributes = super
142
+ enums.each do |name, reflection|
143
+ if (value = attributes.delete(reflection.name.to_s))
144
+ attributes[reflection.id_name.to_s] ||= reflection.id(value)
145
+ end
146
+ end
147
+ attributes
148
+ end
149
+ end
150
+
151
+ def enum(name)
152
+ self.class.enums[name]
153
+ end
154
+
155
+ def enum!(name)
156
+ self.class.enums[name] or
157
+ raise ArgumentError, "no such enum: #{name}"
158
+ end
159
+
160
+ def enum_id(name, value)
161
+ self.class.enum_id(name, value)
162
+ end
163
+
164
+ def read_enum(name)
165
+ reflection = enum!(name)
166
+ id = read_attribute(reflection.id_name)
167
+ reflection.value(id)
168
+ end
169
+
170
+ def query_enum(name)
171
+ reflection = enum!(name)
172
+ id = read_attribute(reflection.id_name)
173
+ !!reflection.value(id)
174
+ end
175
+
176
+ def write_enum(name, value)
177
+ reflection = enum!(name)
178
+ id = reflection.id(value)
179
+ write_attribute(reflection.id_name, id)
180
+ value
181
+ end
182
+
183
+ def enum_changed?(name)
184
+ reflection = enum!(name)
185
+ attribute_changed?(reflection.id_name.to_s)
186
+ end
187
+
188
+ def enum_was(name)
189
+ reflection = enum!(name)
190
+ id = attribute_was(reflection.id_name.to_s)
191
+ reflection.value(id)
192
+ end
193
+
194
+ def enum_change(name)
195
+ reflection = enum!(name)
196
+ change = attribute_change(reflection.id_name.to_s) or
197
+ return nil
198
+ old_id, new_id = *change
199
+ [reflection.value(old_id), reflection.value(new_id)]
200
+ end
201
+
202
+ def read_attribute(name)
203
+ reflection = enum(name) or
204
+ return super
205
+ read_enum(name)
206
+ end
207
+
208
+ def write_attribute(name, value)
209
+ reflection = enum(name) or
210
+ return super
211
+ write_enum(name, value)
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,52 @@
1
+ module EnumTable
2
+ class Reflection
3
+ def initialize(name, options={})
4
+ @name = name
5
+ @id_name = options[:id_name] || :"#{name}_id"
6
+ @type = options[:type] || :symbol
7
+ @type == :string || @type == :symbol or
8
+ raise ArgumentError, "invalid type: #{type.inspect}"
9
+
10
+ @strings_to_ids = {}
11
+ @values_to_ids = {}
12
+ @ids_to_values = {}
13
+ end
14
+
15
+ def initialize_copy(other)
16
+ @name = other.name
17
+ @id_name = other.id_name
18
+ @type = other.type
19
+
20
+ @strings_to_ids = other.instance_variable_get(:@strings_to_ids).dup
21
+ @values_to_ids = other.instance_variable_get(:@values_to_ids).dup
22
+ @ids_to_values = other.instance_variable_get(:@ids_to_values).dup
23
+ end
24
+
25
+ attr_reader :name
26
+ attr_accessor :id_name, :type
27
+
28
+ def add_value(id, value)
29
+ @strings_to_ids[value.to_s] = id
30
+
31
+ cast_value = @type == :string ? value.to_s : value.to_sym
32
+ @values_to_ids[cast_value] = id
33
+ @ids_to_values[id] = cast_value
34
+ end
35
+
36
+ def id(value)
37
+ if value.is_a?(String) || type == :string
38
+ @strings_to_ids[value.to_s.strip]
39
+ else
40
+ @values_to_ids[value]
41
+ end
42
+ end
43
+
44
+ def value(id)
45
+ @ids_to_values[id]
46
+ end
47
+
48
+ def values
49
+ @values_to_ids.keys
50
+ end
51
+ end
52
+ end