enum_table 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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