sequel-table_inheritance 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a970fd131bbbece81669fee96f7ce23277c59b65
4
+ data.tar.gz: 8c0db19ecf4ec040f34a00e71053e8b1c76fc5b3
5
+ SHA512:
6
+ metadata.gz: c3b70ea2393962841410421938bef62f8dabdb5da6af50728334b0c8dbf5ca2e060827cadeef5057e9f40e7b1aee26528dc508c5f3f78c152d39efd7cbab69dc
7
+ data.tar.gz: 85e639a0ccb4f8f5c0e82c955aa9f860e9576e80ac13d4668f1777a53e5b0568cfdd9ea03f312a3460b008b67767ba286d570713a8c5088acb4075d0f1f6ad8c
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /.idea/
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Quinn Harris
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,199 @@
1
+ == Sequel Hybrid Table Inheritance
2
+
3
+ This is a sequel plugin that combines the functionality of the single and class
4
+ table inheritance plugins. This plugin uses the single_table_inheritance plugin
5
+ and should work as a drop in replacement for the class_table_inheritance plugins.
6
+ This allows using new tables for subclasses only when need for additional columns
7
+ or possibly referential integrity to exclusively subclassed rows.
8
+
9
+ == Additional features over the class table inheritance plugin
10
+ For class table inheritance use this provides additional functionality beyond
11
+ the standard class table inheritance plugin including the following:
12
+
13
+ * Eager loading in addition to lazy loading of subclasses
14
+ * Use RETURNING * on insert if available avoiding a select query after new model saves
15
+ * Features found in the single table inheritance not in class table inheritance
16
+ Notably the key_map, key_chooser options and accepting a proc in addition to
17
+ a hash for model_map option
18
+
19
+ == Documentation
20
+ === Overview
21
+
22
+ The hybrid_table_inheritance pluging allows model subclasses to be stored
23
+ in either the same table as the parent model or a different table with a key
24
+ referencing the parent table.
25
+ This combines the functionality of single and class (multiple) table inheritance
26
+ into one plugin. This plugin uses the single_table_inheritance plugin
27
+ and should work as a drop in replacement for the class_table_inheritance plugins.
28
+ This allows introducing new tables only when needed typically for additional
29
+ fields or possibly referential integrity to subclassed objects.
30
+
31
+ === Detail
32
+
33
+ For example, with this hierarchy:
34
+
35
+ Employee
36
+ / \
37
+ Staff Manager
38
+ | |
39
+ Cook Executive
40
+ |
41
+ CEO
42
+
43
+ the following database schema may be used (table - columns):
44
+
45
+ employees :: id, name, kind
46
+ staff :: id, manager_id
47
+ managers :: id, num_staff
48
+ executives :: id, num_managers
49
+
50
+ The hybrid_table_inheritance plugin assumes that the root table
51
+ (e.g. employees) has a primary key field (usually autoincrementing),
52
+ and all other tables have a foreign key of the same name that points
53
+ to the same key in their superclass's table. In this example,
54
+ the employees id column is a primary key and the id column in every
55
+ other table is a foreign key referencing the employees id.
56
+
57
+ In this example the employees table stores Staff model objects and the
58
+ executives table stores CEO model objects.
59
+
60
+ When using the class_table_inheritance plugin, subclasses use joined
61
+ datasets:
62
+
63
+ Employee.dataset.sql
64
+ # SELECT * FROM employees
65
+
66
+ Manager.dataset.sql
67
+ # SELECT employees.id, employees.name, employees.kind,
68
+ # managers.num_staff
69
+ # FROM employees
70
+ # JOIN managers ON (managers.id = employees.id)
71
+
72
+ CEO.dataset.sql
73
+ # SELECT employees.id, employees.name, employees.kind,
74
+ # managers.num_staff, executives.num_managers
75
+ # FROM employees
76
+ # JOIN managers ON (managers.id = employees.id)
77
+ # JOIN executives ON (executives.id = managers.id)
78
+ # WHERE (employees.kind IN ('CEO'))
79
+
80
+ This allows CEO.all to return instances with all attributes
81
+ loaded. The plugin overrides the deleting, inserting, and updating
82
+ in the model to work with multiple tables, by handling each table
83
+ individually.
84
+
85
+ === Subclass loading
86
+
87
+ When model objects are retrieved for a superclass the result could be
88
+ subclass objects needing additional attributes from other tables.
89
+ This plugin can load those additional attributes immediately with eager
90
+ loading or when requested on the object with lazy loading.
91
+
92
+ With eager loading, the additional needed rows will be loaded with the
93
+ all or first methods. Note that eager loading does not work
94
+ with the each method because all of the records must be loaded to
95
+ determine the keys for each subclass query. In that case lazy loading can
96
+ be used or the each method used on the result of the all method.
97
+
98
+ If lazy loading is used the lazy_attributes plugin will be included to
99
+ return subclass specific attributes that were not loaded
100
+ when calling superclass methods (since those wouldn't join
101
+ to the subclass tables). For example:
102
+
103
+ a = Employee.all # [<#Staff>, <#Manager>, <#Executive>]
104
+ a.first.values # {:id=>1, name=>'S', :kind=>'Staff'}
105
+ a.first.manager_id # Loads the manager_id attribute from the database
106
+
107
+ If you want to get all columns in a subclass instance after loading
108
+ via the superclass, call Model#refresh.
109
+
110
+ a = Employee.first
111
+ a.values # {:id=>1, name=>'S', :kind=>'CEO'}
112
+ a.refresh.values # {:id=>1, name=>'S', :kind=>'Executive', :num_staff=>4, :num_managers=>2}
113
+
114
+ The option :subclass_load sets the default subclass loading strategy.
115
+ It accepts :eager, :eager_only, :lazy or :lazy_only with a default of :lazy
116
+ The _only options will only allow that strategy to be used.
117
+ In addition eager or lazy can be called on a dataset to override the default
118
+ strategy used assuming an _only option was not set.
119
+
120
+ === Usage
121
+
122
+ # Use the default of storing the class name in the sti_key
123
+ # column (:kind in this case)
124
+ class Employee < Sequel::Model
125
+ plugin :hybrid_table_inheritance, :key=>:kind
126
+ end
127
+
128
+ # Have subclasses inherit from the appropriate class
129
+ class Staff < Employee; end # uses staff table
130
+ class Cook < Staff; end # cooks table doesn't exist so uses staff table
131
+ class Manager < Employee; end # uses managers table
132
+ class Executive < Manager; end # uses executives table
133
+ class CEO < Executive; end # ceos table doesn't exist so uses executives table
134
+
135
+ # Some examples of using these options:
136
+
137
+ # Specifying the tables with a :table_map hash
138
+ Employee.plugin :hybrid_table_inheritance,
139
+ :table_map=>{:Employee => :employees,
140
+ :Staff => :staff,
141
+ :Cook => :staff,
142
+ :Manager => :managers,
143
+ :Executive => :executives,
144
+ :CEO => :executives }
145
+
146
+ # Using integers to store the class type, with a :model_map hash
147
+ # and an sti_key of :type
148
+ Employee.plugin :hybrid_table_inheritance, :type,
149
+ :model_map=>{1=>:Staff, 2=>:Cook, 3=>:Manager, 4=>:Executive, 5=>:CEO}
150
+
151
+ # Using non-class name strings
152
+ Employee.plugin :hybrid_table_inheritance, :key=>:type,
153
+ :model_map=>{'staff'=>:Staff, 'cook staff'=>:Cook, 'supervisor'=>:Manager}
154
+
155
+ # By default the plugin sets the respective column value
156
+ # when a new instance is created.
157
+ Cook.create.type == 'cook staff'
158
+ Manager.create.type == 'supervisor'
159
+
160
+ # You can customize this behavior with the :key_chooser option.
161
+ # This is most useful when using a non-bijective mapping.
162
+ Employee.plugin :hybrid_table_inheritance, :key=>:type,
163
+ :model_map=>{'cook staff'=>:Cook, 'supervisor'=>:Manager},
164
+ :key_chooser=>proc{|instance| instance.model.sti_key_map[instance.model.to_s].first || 'stranger' }
165
+
166
+ # Using custom procs, with :model_map taking column values
167
+ # and yielding either a class, string, symbol, or nil,
168
+ # and :key_map taking a class object and returning the column
169
+ # value to use
170
+ Employee.plugin :single_table_inheritance, :key=>:type,
171
+ :model_map=>proc{|v| v.reverse},
172
+ :key_map=>proc{|klass| klass.name.reverse}
173
+
174
+ # You can use the same class for multiple values.
175
+ # This is mainly useful when the sti_key column contains multiple values
176
+ # which are different but do not require different code.
177
+ Employee.plugin :single_table_inheritance, :key=>:type,
178
+ :model_map=>{'staff' => "Staff",
179
+ 'manager' => "Manager",
180
+ 'overpayed staff' => "Staff",
181
+ 'underpayed staff' => "Staff"}
182
+
183
+ One minor issue to note is that if you specify the <tt>:key_map</tt>
184
+ option as a hash, instead of having it inferred from the <tt>:model_map</tt>,
185
+ you should only use class name strings as keys, you should not use symbols
186
+ as keys.
187
+
188
+ == Options
189
+ :key :: column symbol that holds the key that identifies the class to use.
190
+ Necessary if you want to call model methods on a superclass
191
+ that return subclass instances
192
+ :model_map :: Hash or proc mapping the key column values to model class names.
193
+ :key_map :: Hash or proc mapping model class names to key column values.
194
+ Each value or return is an array of possible key column values.
195
+ :key_chooser :: proc returning key for the provided model instance
196
+ :table_map :: Hash with class name symbols keys mapping to table name symbol values
197
+ Overrides implicit table names
198
+ :subclass_load :: subclass loading strategy, defaults to :lazy
199
+ options: :eager, :eager_only, :lazy or :lazy_only
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "sequel/plugins/hybrid_table_inheritance"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,480 @@
1
+ module Sequel
2
+ module Plugins
3
+ # = Overview
4
+ #
5
+ # The hybrid_table_inheritance pluging allows model subclasses to be stored
6
+ # in either the same table as the parent model or a different table with a key
7
+ # referencing the parent table.
8
+ # This combines the functionality of single and class (multiple) table inheritance
9
+ # into one plugin. This plugin uses the single_table_inheritance plugin
10
+ # and should work as a drop in replacement for the class_table_inheritance plugins.
11
+ # This allows introducing new tables only when needed typically for additional
12
+ # fields or possibly referential integrity to subclassed objects.
13
+ #
14
+ # = Detail
15
+ #
16
+ # For example, with this hierarchy:
17
+ #
18
+ # Employee
19
+ # / \
20
+ # Staff Manager
21
+ # | |
22
+ # Cook Executive
23
+ # |
24
+ # CEO
25
+ #
26
+ # the following database schema may be used (table - columns):
27
+ #
28
+ # employees :: id, name, kind
29
+ # staff :: id, manager_id
30
+ # managers :: id, num_staff
31
+ # executives :: id, num_managers
32
+ #
33
+ # The hybrid_table_inheritance plugin assumes that the root table
34
+ # (e.g. employees) has a primary key field (usually autoincrementing),
35
+ # and all other tables have a foreign key of the same name that points
36
+ # to the same key in their superclass's table. In this example,
37
+ # the employees id column is a primary key and the id column in every
38
+ # other table is a foreign key referencing the employees id.
39
+ #
40
+ # In this example the employees table stores Staff model objects and the
41
+ # executives table stores CEO model objects.
42
+ #
43
+ # When using the class_table_inheritance plugin, subclasses use joined
44
+ # datasets:
45
+ #
46
+ # Employee.dataset.sql
47
+ # # SELECT * FROM employees
48
+ #
49
+ # Manager.dataset.sql
50
+ # # SELECT employees.id, employees.name, employees.kind,
51
+ # # managers.num_staff
52
+ # # FROM employees
53
+ # # JOIN managers ON (managers.id = employees.id)
54
+ #
55
+ # CEO.dataset.sql
56
+ # # SELECT employees.id, employees.name, employees.kind,
57
+ # # managers.num_staff, executives.num_managers
58
+ # # FROM employees
59
+ # # JOIN managers ON (managers.id = employees.id)
60
+ # # JOIN executives ON (executives.id = managers.id)
61
+ # # WHERE (employees.kind IN ('CEO'))
62
+ #
63
+ # This allows CEO.all to return instances with all attributes
64
+ # loaded. The plugin overrides the deleting, inserting, and updating
65
+ # in the model to work with multiple tables, by handling each table
66
+ # individually.
67
+ #
68
+ # = Subclass loading
69
+ #
70
+ # When model objects are retrieved for a superclass the result could be
71
+ # subclass objects needing additional attributes from other tables.
72
+ # This plugin can load those additional attributes immediately with eager
73
+ # loading or when requested on the object with lazy loading.
74
+ #
75
+ # With eager loading, the additional needed rows will be loaded with the
76
+ # all or first methods. Note that eager loading does not work
77
+ # with the each method because all of the records must be loaded to
78
+ # determine the keys for each subclass query. In that case lazy loading can
79
+ # be used or the each method used on the result of the all method.
80
+ #
81
+ # If lazy loading is used the lazy_attributes plugin will be included to
82
+ # return subclass specific attributes that were not loaded
83
+ # when calling superclass methods (since those wouldn't join
84
+ # to the subclass tables). For example:
85
+ #
86
+ # a = Employee.all # [<#Staff>, <#Manager>, <#Executive>]
87
+ # a.first.values # {:id=>1, name=>'S', :kind=>'Staff'}
88
+ # a.first.manager_id # Loads the manager_id attribute from the database
89
+ #
90
+ # If you want to get all columns in a subclass instance after loading
91
+ # via the superclass, call Model#refresh.
92
+ #
93
+ # a = Employee.first
94
+ # a.values # {:id=>1, name=>'S', :kind=>'CEO'}
95
+ # a.refresh.values # {:id=>1, name=>'S', :kind=>'Executive', :num_staff=>4, :num_managers=>2}
96
+ #
97
+ # The option :subclass_load sets the default subclass loading strategy.
98
+ # It accepts :eager, :eager_only, :lazy or :lazy_only with a default of :lazy
99
+ # The _only options will only allow that strategy to be used.
100
+ # In addition eager or lazy can be called on a dataset to override the default
101
+ # strategy used assuming an _only option was not set.
102
+ #
103
+ # = Usage
104
+ #
105
+ # # Use the default of storing the class name in the sti_key
106
+ # # column (:kind in this case)
107
+ # class Employee < Sequel::Model
108
+ # plugin :hybrid_table_inheritance, :key=>:kind
109
+ # end
110
+ #
111
+ # # Have subclasses inherit from the appropriate class
112
+ # class Staff < Employee; end # uses staff table
113
+ # class Cook < Staff; end # cooks table doesn't exist so uses staff table
114
+ # class Manager < Employee; end # uses managers table
115
+ # class Executive < Manager; end # uses executives table
116
+ # class CEO < Executive; end # ceos table doesn't exist so uses executives table
117
+ #
118
+ # # Some examples of using these options:
119
+ #
120
+ # # Specifying the tables with a :table_map hash
121
+ # Employee.plugin :hybrid_table_inheritance,
122
+ # :table_map=>{:Employee => :employees,
123
+ # :Staff => :staff,
124
+ # :Cook => :staff,
125
+ # :Manager => :managers,
126
+ # :Executive => :executives,
127
+ # :CEO => :executives }
128
+ #
129
+ # # Using integers to store the class type, with a :model_map hash
130
+ # # and an sti_key of :type
131
+ # Employee.plugin :hybrid_table_inheritance, :type,
132
+ # :model_map=>{1=>:Staff, 2=>:Cook, 3=>:Manager, 4=>:Executive, 5=>:CEO}
133
+ #
134
+ # # Using non-class name strings
135
+ # Employee.plugin :hybrid_table_inheritance, :key=>:type,
136
+ # :model_map=>{'staff'=>:Staff, 'cook staff'=>:Cook, 'supervisor'=>:Manager}
137
+ #
138
+ # # By default the plugin sets the respective column value
139
+ # # when a new instance is created.
140
+ # Cook.create.type == 'cook staff'
141
+ # Manager.create.type == 'supervisor'
142
+ #
143
+ # # You can customize this behavior with the :key_chooser option.
144
+ # # This is most useful when using a non-bijective mapping.
145
+ # Employee.plugin :hybrid_table_inheritance, :key=>:type,
146
+ # :model_map=>{'cook staff'=>:Cook, 'supervisor'=>:Manager},
147
+ # :key_chooser=>proc{|instance| instance.model.sti_key_map[instance.model.to_s].first || 'stranger' }
148
+ #
149
+ # # Using custom procs, with :model_map taking column values
150
+ # # and yielding either a class, string, symbol, or nil,
151
+ # # and :key_map taking a class object and returning the column
152
+ # # value to use
153
+ # Employee.plugin :single_table_inheritance, :key=>:type,
154
+ # :model_map=>proc{|v| v.reverse},
155
+ # :key_map=>proc{|klass| klass.name.reverse}
156
+ #
157
+ # # You can use the same class for multiple values.
158
+ # # This is mainly useful when the sti_key column contains multiple values
159
+ # # which are different but do not require different code.
160
+ # Employee.plugin :single_table_inheritance, :key=>:type,
161
+ # :model_map=>{'staff' => "Staff",
162
+ # 'manager' => "Manager",
163
+ # 'overpayed staff' => "Staff",
164
+ # 'underpayed staff' => "Staff"}
165
+ #
166
+ # One minor issue to note is that if you specify the <tt>:key_map</tt>
167
+ # option as a hash, instead of having it inferred from the <tt>:model_map</tt>,
168
+ # you should only use class name strings as keys, you should not use symbols
169
+ # as keys.
170
+ module HybridTableInheritance
171
+ # The class_table_inheritance plugin requires the lazy_attributes plugin
172
+ # to handle lazily-loaded attributes for subclass instances returned
173
+ # by superclass methods.
174
+ def self.apply(model, opts = OPTS)
175
+ model.plugin :single_table_inheritance, nil
176
+ model.plugin :lazy_attributes unless opts[:subclass_load] == :eager_only
177
+ end
178
+
179
+ # Setup the plugin using the following options:
180
+ # :key :: column symbol that holds the key that identifies the class to use.
181
+ # Necessary if you want to call model methods on a superclass
182
+ # that return subclass instances
183
+ # :model_map :: Hash or proc mapping the key column values to model class names.
184
+ # :key_map :: Hash or proc mapping model class names to key column values.
185
+ # Each value or return is an array of possible key column values.
186
+ # :key_chooser :: proc returning key for the provided model instance
187
+ # :table_map :: Hash with class name symbols keys mapping to table name symbol values
188
+ # Overrides implicit table names
189
+ # :subclass_load :: subclass loading strategy, defaults to :lazy
190
+ # options: :eager, :eager_only, :lazy or :lazy_only
191
+ def self.configure(model, opts = OPTS)
192
+ SingleTableInheritance.configure model, opts[:key], opts
193
+
194
+ model.instance_eval do
195
+ @cti_subclass_load = opts[:subclass_load]
196
+ @cti_subclass_datasets = {}
197
+ @cti_models = [self]
198
+ @cti_tables = [table_name]
199
+ @cti_instance_dataset = db.from(table_name)
200
+ @cti_table_columns = columns
201
+ @cti_table_map = opts[:table_map] || {}
202
+ end
203
+ end
204
+
205
+ module ClassMethods
206
+ # An array of each model in the inheritance hierarchy that uses an
207
+ # backed by a new table.
208
+ attr_reader :cti_models
209
+
210
+ # The parent/root/base model for this class table inheritance hierarchy.
211
+ # This is the only model in the hierarchy that loads the
212
+ # class_table_inheritance plugin.
213
+ # Only needed to be compatible with class_table_inheritance plugin
214
+ def cti_base_model
215
+ @cti_models.first
216
+ end
217
+
218
+ # Last model in the inheritance hierarchy to use a new table
219
+ def cti_table_model
220
+ @cti_models.last
221
+ end
222
+
223
+ # A hash with subclass models keys and datasets to load that subclass
224
+ # assuming the current model has already been loaded.
225
+ # Used for eager loading
226
+ attr_reader :cti_subclass_datasets
227
+
228
+ # Eager loading option
229
+ attr_reader :cti_subclass_load
230
+
231
+ # Hash with table name symbol keys and arrays of column symbol values,
232
+ # giving the columns to update in each backing database table.
233
+ # Only needed to be compatible with class_table_inheritance plugin
234
+ def cti_columns
235
+ h = {}
236
+ cti_models.each { |m| h[m.table_name] = m.cti_table_columns }
237
+ h
238
+ end
239
+
240
+ # An array of column symbols for the backing database table,
241
+ # giving the columns to update in each backing database table.
242
+ attr_reader :cti_table_columns
243
+
244
+ # The dataset that table instance datasets are based on.
245
+ # Used for database modifications
246
+ attr_reader :cti_instance_dataset
247
+
248
+ # An array of table symbols that back this model. The first is
249
+ # cti_base_model table symbol, and the last is the current model
250
+ # table symbol.
251
+ attr_reader :cti_tables
252
+
253
+ # A hash with class name symbol keys and table name symbol values.
254
+ # Specified with the :table_map option to the plugin, and used if
255
+ # the implicit naming is incorrect.
256
+ attr_reader :cti_table_map
257
+
258
+ def inherited(subclass)
259
+ ds = sti_dataset
260
+
261
+ # Prevent inherited in model/base.rb from setting the dataset
262
+ subclass.instance_eval { @dataset = nil }
263
+
264
+ @cti_tables.push ds.first_source_alias # Kludge to change filter to use root table
265
+ super # Call single_table_inheritance
266
+ @cti_tables.pop
267
+
268
+ cm = cti_models
269
+ ctm = cti_table_map
270
+ ct = cti_tables
271
+ ctc = cti_table_columns
272
+ cid = cti_instance_dataset
273
+ pk = primary_key
274
+ csl = cti_subclass_load
275
+
276
+ # Set table if this is a class table inheritance
277
+ table = nil
278
+ columns = nil
279
+ if (n = subclass.name) && !n.empty?
280
+ if table = ctm[n.to_sym]
281
+ columns = db.from(table).columns
282
+ else
283
+ table = subclass.implicit_table_name
284
+ columns = db.from(table).columns rescue nil
285
+ table = nil if !columns || columns.empty?
286
+ end
287
+ end
288
+ table = nil if table && (table == table_name)
289
+
290
+ subclass.instance_eval do
291
+ @cti_table_map = ctm
292
+ @cti_subclass_load = csl
293
+ @cti_subclass_datasets = {}
294
+
295
+ if table
296
+ if ct.length == 1
297
+ ds = ds.select(*self.columns.map{|cc| Sequel.qualify(table_name, Sequel.identifier(cc))})
298
+ end
299
+ sel_app = (columns - [pk]).map{|cc| Sequel.qualify(table, Sequel.identifier(cc))}
300
+ @sti_dataset = ds.join(table, pk=>pk).select_append(*sel_app)
301
+ set_dataset(@sti_dataset)
302
+ set_columns(self.columns)
303
+ dataset.row_proc = lambda{|r| subclass.sti_load(r)}
304
+
305
+ @cti_models = cm + [self]
306
+ @cti_tables = ct + [table]
307
+ @cti_table_columns = columns
308
+ @cti_instance_dataset = db.from(table_name)
309
+
310
+ unless csl == :lazy_only
311
+ cm.each do |model|
312
+ sd = model.instance_variable_get(:@cti_subclass_datasets)
313
+ unless d = sd[cm.last]
314
+ sd[self] = db.from(table).select(*columns.map{|cc| Sequel.qualify(table_name, Sequel.identifier(cc))})
315
+ else
316
+ sd[self] = d.join(table, pk=>pk).select_append(*sel_app)
317
+ end
318
+ end
319
+ end
320
+ unless csl == :eager_only
321
+ (columns - [pk]).each{|a| define_lazy_attribute_getter(a, :dataset=>dataset, :table=>table)}
322
+ end
323
+ cti_tables.reverse.each do |ct|
324
+ db.schema(ct).each{|sk,v| db_schema[sk] = v}
325
+ end
326
+ else
327
+ @cti_models = cm
328
+ @cti_tables = ct
329
+ @cti_table_columns = ctc
330
+ @cti_instance_dataset = cid
331
+ end
332
+ end
333
+ end
334
+
335
+ # The table name for the current model class's main table.
336
+ def table_name
337
+ cti_tables ? cti_tables.last : super
338
+ end
339
+
340
+ def sti_class_from_key(key)
341
+ sti_class(sti_model_map[key])
342
+ end
343
+ end
344
+
345
+ module InstanceMethods
346
+ # Delete the row from all backing tables, starting from the
347
+ # most recent table and going through all superclasses.
348
+ def delete
349
+ raise Sequel::Error, "can't delete frozen object" if frozen?
350
+ model.cti_models.reverse.each do |m|
351
+ cti_this(m).delete
352
+ end
353
+ self
354
+ end
355
+
356
+ private
357
+
358
+ def cti_this(model)
359
+ use_server(model.cti_instance_dataset.filter(model.primary_key_hash(pk)))
360
+ end
361
+
362
+ # Set the sti_key column based on the sti_key_map.
363
+ def _before_validation
364
+ if new? && (set = self[model.sti_key])
365
+ exp = model.sti_key_chooser.call(self)
366
+ if set != exp
367
+ set_table = model.sti_class_from_key(set).table_name
368
+ exp_table = model.sti_class_from_key(exp).table_name
369
+ set_column_value("#{model.sti_key}=", exp) if set_table != exp_table
370
+ end
371
+ end
372
+ super
373
+ end
374
+
375
+ # Insert rows into all backing tables, using the columns
376
+ # in each table.
377
+ def _insert
378
+ return super if model.cti_tables.length == 1
379
+ model.cti_models.each do |m|
380
+ v = {}
381
+ m.cti_table_columns.each{|c| v[c] = @values[c] if @values.include?(c)}
382
+ ds = use_server(m.cti_instance_dataset)
383
+ if ds.supports_insert_select? && (h = ds.insert_select(v))
384
+ @values.merge!(h)
385
+ else
386
+ nid = ds.insert(v)
387
+ @values[primary_key] ||= nid
388
+ end
389
+ end
390
+ db.dataset.supports_insert_select? ? nil : @values[primary_key]
391
+ end
392
+
393
+ # Update rows in all backing tables, using the columns in each table.
394
+ def _update(columns)
395
+ model.cti_models.each do |m|
396
+ h = {}
397
+ m.cti_table_columns.each{|c| h[c] = columns[c] if columns.include?(c)}
398
+ cti_this(m).update(h) unless h.empty?
399
+ end
400
+ end
401
+ end
402
+
403
+ module DatasetMethods
404
+ def single_record
405
+ post_load_record(super)
406
+ end
407
+
408
+ def with_sql_first(sql)
409
+ post_load_record(super)
410
+ end
411
+
412
+ # Set dataset to use eager loading
413
+ def eager
414
+ raise Error, "eager loading disabled" if model.cti_subclass_load == :lazy_only
415
+ clone(:eager_load => true)
416
+ end
417
+
418
+ # Set dataset to use lazy loading
419
+ def lazy
420
+ raise Error, "lazy loading disabled" if model.cti_subclass_load == :eager_only
421
+ clone(:eager_load => false)
422
+ end
423
+
424
+ # Return true if eager loading will be used, false/nil for lazy loading
425
+ def uses_eager_load?
426
+ return opts[:eager_load] unless opts[:eager_load].nil?
427
+ [:eager, :eager_only].include?(model.cti_subclass_load)
428
+ end
429
+
430
+ private
431
+
432
+ def subclass_dataset(m, keys)
433
+ ds = model.cti_subclass_datasets[m]
434
+ ds = ds.filter(m.qualified_primary_key_hash(keys, ds.first_source_alias))
435
+ ds
436
+ end
437
+
438
+ def post_load_record(r)
439
+ return r unless r && uses_eager_load?
440
+ ds = subclass_dataset(r.model.cti_table_model, r.pk)
441
+ r.values.merge!(ds.limit(1).first)
442
+ r
443
+ end
444
+
445
+ def post_load(records)
446
+ super
447
+ return unless uses_eager_load?
448
+
449
+ subclass_datasets = model.cti_subclass_datasets
450
+
451
+ table_key_map = Hash.new
452
+ records.each do |r|
453
+ model = r.model.cti_table_model
454
+ next unless subclass_datasets.key?(model)
455
+ table_key_map[model] = { } unless table_key_map.key?(model)
456
+ table_key_map[model][r.pk] = r
457
+ end
458
+
459
+ pkc = model.primary_key
460
+ table_key_map.each do |model, id_map|
461
+ ds = subclass_dataset(model, id_map.keys)
462
+ applied_set = {}
463
+ ds.all do |r|
464
+ pkv = pkc.is_a?(Array) ? pkc.map{|k| r[k]} : r[pkc]
465
+ m = id_map[pkv]
466
+ if applied_set.key?(pkv)
467
+ # Multiple rows for one row in original query
468
+ # insert is O(n) but needed if dataset is ordered.
469
+ # This code path should seldom if ever get used
470
+ records.insert(records.index(m)+1, m = m.dup)
471
+ end
472
+ applied_set[pkv] = true
473
+ m.values.merge!(r)
474
+ end
475
+ end
476
+ end
477
+ end
478
+ end
479
+ end
480
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ #require 'sequel/table_inheritance/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "sequel-table_inheritance"
8
+ s.version = '0.1.0' #Sequel::TableInheritance::VERSION
9
+ s.authors = ["Quinn Harris"]
10
+ s.email = ["sequel@quinnharris.me"]
11
+
12
+ s.summary = "Alternative to single and class table inheritance plugins for sequel"
13
+ s.description = s.summary
14
+ s.homepage = "https://github.com/QuinnHarris/sequel-table_inheritance"
15
+ s.license = "MIT"
16
+
17
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ s.bindir = "bin"
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency "bundler", "~> 1.9"
22
+ s.add_development_dependency "rake", "~> 10.0"
23
+ s.add_development_dependency "rspec"
24
+
25
+ s.add_dependency "sequel", "~> 4.19"
26
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sequel-table_inheritance
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Quinn Harris
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.9'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sequel
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '4.19'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '4.19'
69
+ description: Alternative to single and class table inheritance plugins for sequel
70
+ email:
71
+ - sequel@quinnharris.me
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - Gemfile
78
+ - LICENSE.txt
79
+ - README.rdoc
80
+ - Rakefile
81
+ - bin/console
82
+ - bin/setup
83
+ - lib/sequel/plugins/hybrid_table_inheritance.rb
84
+ - sequel-table_inheritance.gemspec
85
+ homepage: https://github.com/QuinnHarris/sequel-table_inheritance
86
+ licenses:
87
+ - MIT
88
+ metadata: {}
89
+ post_install_message:
90
+ rdoc_options: []
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ requirements: []
104
+ rubyforge_project:
105
+ rubygems_version: 2.4.5
106
+ signing_key:
107
+ specification_version: 4
108
+ summary: Alternative to single and class table inheritance plugins for sequel
109
+ test_files: []