sequel 4.23.0 → 4.24.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +26 -0
- data/Rakefile +1 -1
- data/doc/release_notes/4.24.0.txt +99 -0
- data/doc/sql.rdoc +10 -1
- data/lib/sequel/adapters/jdbc.rb +7 -0
- data/lib/sequel/adapters/jdbc/cubrid.rb +1 -1
- data/lib/sequel/adapters/jdbc/db2.rb +1 -1
- data/lib/sequel/adapters/jdbc/derby.rb +1 -1
- data/lib/sequel/adapters/jdbc/h2.rb +1 -1
- data/lib/sequel/adapters/jdbc/hsqldb.rb +1 -1
- data/lib/sequel/adapters/jdbc/mssql.rb +1 -1
- data/lib/sequel/adapters/jdbc/mysql.rb +2 -2
- data/lib/sequel/adapters/jdbc/oracle.rb +1 -1
- data/lib/sequel/adapters/jdbc/sqlanywhere.rb +1 -1
- data/lib/sequel/adapters/jdbc/sqlite.rb +1 -1
- data/lib/sequel/adapters/postgres.rb +14 -6
- data/lib/sequel/adapters/shared/mssql.rb +1 -1
- data/lib/sequel/core.rb +12 -1
- data/lib/sequel/database/connecting.rb +1 -2
- data/lib/sequel/extensions/pg_inet_ops.rb +200 -0
- data/lib/sequel/plugins/association_pks.rb +63 -18
- data/lib/sequel/plugins/auto_validations.rb +43 -9
- data/lib/sequel/plugins/class_table_inheritance.rb +236 -179
- data/lib/sequel/plugins/update_refresh.rb +26 -1
- data/lib/sequel/plugins/validation_helpers.rb +7 -2
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/oracle_spec.rb +1 -1
- data/spec/adapters/postgres_spec.rb +61 -0
- data/spec/core_extensions_spec.rb +5 -1
- data/spec/extensions/association_pks_spec.rb +73 -1
- data/spec/extensions/auto_validations_spec.rb +34 -0
- data/spec/extensions/class_table_inheritance_spec.rb +58 -54
- data/spec/extensions/pg_inet_ops_spec.rb +101 -0
- data/spec/extensions/spec_helper.rb +5 -5
- data/spec/extensions/update_refresh_spec.rb +12 -0
- data/spec/extensions/validation_helpers_spec.rb +7 -0
- data/spec/integration/plugin_test.rb +48 -13
- metadata +6 -4
- data/lib/sequel/adapters/db2.rb +0 -229
- data/lib/sequel/adapters/dbi.rb +0 -102
@@ -1,10 +1,9 @@
|
|
1
1
|
module Sequel
|
2
2
|
module Plugins
|
3
|
-
# The association_pks plugin adds
|
3
|
+
# The association_pks plugin adds association_pks and association_pks=
|
4
4
|
# instance methods to the model class for each association added. These
|
5
5
|
# methods allow for easily returning the primary keys of the associated
|
6
|
-
# objects, and easily modifying
|
7
|
-
# keys to just the ones given:
|
6
|
+
# objects, and easily modifying which objects are associated:
|
8
7
|
#
|
9
8
|
# Artist.one_to_many :albums
|
10
9
|
# artist = Artist[1]
|
@@ -21,6 +20,13 @@ module Sequel
|
|
21
20
|
# not call any callbacks. If you have any association callbacks,
|
22
21
|
# you probably should not use the setter methods.
|
23
22
|
#
|
23
|
+
# If an association uses the :delay option, you can set the associated
|
24
|
+
# pks for new objects, and the setting will not be persisted until after the
|
25
|
+
# object has been created in the database. Additionally, if an association
|
26
|
+
# uses the :delay=>:all option, you can set the associated pks for existing
|
27
|
+
# objects, and the setting will not be persisted until after the object has
|
28
|
+
# been saved.
|
29
|
+
#
|
24
30
|
# Usage:
|
25
31
|
#
|
26
32
|
# # Make all model subclass *_to_many associations have association_pks
|
@@ -35,14 +41,9 @@ module Sequel
|
|
35
41
|
private
|
36
42
|
|
37
43
|
# Define a association_pks method using the block for the association reflection
|
38
|
-
def
|
39
|
-
association_module_def(:"#{singularize(opts[:name])}_pks", opts
|
40
|
-
|
41
|
-
|
42
|
-
# Define a association_pks= method using the block for the association reflection,
|
43
|
-
# if the association is not read only.
|
44
|
-
def def_association_pks_setter(opts, &block)
|
45
|
-
association_module_def(:"#{singularize(opts[:name])}_pks=", opts, &block) unless opts[:read_only]
|
44
|
+
def def_association_pks_methods(opts)
|
45
|
+
association_module_def(:"#{singularize(opts[:name])}_pks", opts){_association_pks_getter(opts)}
|
46
|
+
association_module_def(:"#{singularize(opts[:name])}_pks=", opts){|pks| _association_pks_setter(opts, pks)} unless opts[:read_only]
|
46
47
|
end
|
47
48
|
|
48
49
|
# Add a getter that checks the join table for matching records and
|
@@ -53,24 +54,24 @@ module Sequel
|
|
53
54
|
return if opts[:type] == :one_through_one
|
54
55
|
|
55
56
|
# Grab values from the reflection so that the hash lookup only needs to be
|
56
|
-
# done once instead of inside
|
57
|
+
# done once instead of inside every method call.
|
57
58
|
lk, lpk, rk = opts.values_at(:left_key, :left_primary_key, :right_key)
|
58
59
|
clpk = lpk.is_a?(Array)
|
59
60
|
crk = rk.is_a?(Array)
|
60
61
|
|
61
|
-
if clpk
|
62
|
-
|
62
|
+
opts[:pks_getter] = if clpk
|
63
|
+
lambda do
|
63
64
|
h = {}
|
64
65
|
lk.zip(lpk).each{|k, pk| h[k] = get_column_value(pk)}
|
65
66
|
_join_table_dataset(opts).filter(h).select_map(rk)
|
66
67
|
end
|
67
68
|
else
|
68
|
-
|
69
|
+
lambda do
|
69
70
|
_join_table_dataset(opts).filter(lk=>get_column_value(lpk)).select_map(rk)
|
70
71
|
end
|
71
72
|
end
|
72
73
|
|
73
|
-
|
74
|
+
opts[:pks_setter] = lambda do |pks|
|
74
75
|
pks = send(crk ? :convert_cpk_array : :convert_pk_array, opts, pks)
|
75
76
|
checked_transaction do
|
76
77
|
if clpk
|
@@ -89,21 +90,24 @@ module Sequel
|
|
89
90
|
ds.import(key_columns, key_array)
|
90
91
|
end
|
91
92
|
end
|
93
|
+
|
94
|
+
def_association_pks_methods(opts)
|
92
95
|
end
|
93
96
|
|
94
97
|
# Add a getter that checks the association dataset and a setter
|
95
98
|
# that updates the associated table.
|
96
99
|
def def_one_to_many(opts)
|
97
100
|
super
|
101
|
+
|
98
102
|
return if opts[:type] == :one_to_one
|
99
103
|
|
100
104
|
key = opts[:key]
|
101
105
|
|
102
|
-
|
106
|
+
opts[:pks_getter] = lambda do
|
103
107
|
send(opts.dataset_method).select_map(opts.associated_class.primary_key)
|
104
108
|
end
|
105
109
|
|
106
|
-
|
110
|
+
opts[:pks_setter] = lambda do |pks|
|
107
111
|
primary_key = opts.associated_class.primary_key
|
108
112
|
|
109
113
|
pks = if primary_key.is_a?(Array)
|
@@ -132,12 +136,53 @@ module Sequel
|
|
132
136
|
ds.exclude(pkh).update(nh)
|
133
137
|
end
|
134
138
|
end
|
139
|
+
|
140
|
+
def_association_pks_methods(opts)
|
135
141
|
end
|
136
142
|
end
|
137
143
|
|
138
144
|
module InstanceMethods
|
145
|
+
# After creating an object, if there are any saved association pks,
|
146
|
+
# call the related association pks setters.
|
147
|
+
def after_save
|
148
|
+
if assoc_pks = @_association_pks
|
149
|
+
assoc_pks.each do |name, pks|
|
150
|
+
instance_exec(pks, &model.association_reflection(name)[:pks_setter]) unless pks.empty?
|
151
|
+
end
|
152
|
+
@_association_pks = nil
|
153
|
+
end
|
154
|
+
super
|
155
|
+
end
|
156
|
+
|
139
157
|
private
|
140
158
|
|
159
|
+
# Return the primary keys of the associated objects.
|
160
|
+
# If the receiver is a new object, return any saved
|
161
|
+
# pks, or an empty array if no pks have been saved.
|
162
|
+
def _association_pks_getter(opts)
|
163
|
+
delay = opts[:delay_pks]
|
164
|
+
if new? && delay
|
165
|
+
(@_association_pks ||= {})[opts[:name]] ||= []
|
166
|
+
elsif delay == :always && @_association_pks && (objs = @_association_pks[opts[:name]])
|
167
|
+
objs
|
168
|
+
else
|
169
|
+
instance_exec(&opts[:pks_getter])
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Update which objects are associated to the receiver.
|
174
|
+
# If the receiver is a new object, save the pks
|
175
|
+
# so the update can happen after the received has been saved.
|
176
|
+
def _association_pks_setter(opts, pks)
|
177
|
+
delay = opts[:delay_pks]
|
178
|
+
if (new? && delay) || (delay == :always)
|
179
|
+
modified!
|
180
|
+
(@_association_pks ||= {})[opts[:name]] = pks
|
181
|
+
else
|
182
|
+
instance_exec(pks, &opts[:pks_setter])
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
141
186
|
# If any of associated class's composite primary key column types is integer,
|
142
187
|
# typecast the appropriate values to integer before using them.
|
143
188
|
def convert_cpk_array(opts, cpks)
|
@@ -36,6 +36,13 @@ module Sequel
|
|
36
36
|
# This is useful if you want to enforce that NOT NULL string columns do not
|
37
37
|
# allow empty values.
|
38
38
|
#
|
39
|
+
# You can also supply hashes to pass options through to the underlying validators:
|
40
|
+
#
|
41
|
+
# Model.plugin :auto_validations, unique_opts: {only_if_modified: true}
|
42
|
+
#
|
43
|
+
# This works for unique_opts, max_length_opts, schema_types_opts,
|
44
|
+
# explicit_not_null_opts, and not_null_opts.
|
45
|
+
#
|
39
46
|
# Usage:
|
40
47
|
#
|
41
48
|
# # Make all model subclass use auto validations (called before loading subclasses)
|
@@ -44,6 +51,12 @@ module Sequel
|
|
44
51
|
# # Make the Album class use auto validations
|
45
52
|
# Album.plugin :auto_validations
|
46
53
|
module AutoValidations
|
54
|
+
NOT_NULL_OPTIONS = {:from=>:values}.freeze
|
55
|
+
EXPLICIT_NOT_NULL_OPTIONS = {:from=>:values, :allow_missing=>true}.freeze
|
56
|
+
MAX_LENGTH_OPTIONS = {:from=>:values, :allow_nil=>true}.freeze
|
57
|
+
SCHEMA_TYPES_OPTIONS = NOT_NULL_OPTIONS
|
58
|
+
UNIQUE_OPTIONS = NOT_NULL_OPTIONS
|
59
|
+
|
47
60
|
def self.apply(model, opts=OPTS)
|
48
61
|
model.instance_eval do
|
49
62
|
plugin :validation_helpers
|
@@ -53,6 +66,14 @@ module Sequel
|
|
53
66
|
@auto_validate_max_length_columns = []
|
54
67
|
@auto_validate_unique_columns = []
|
55
68
|
@auto_validate_types = true
|
69
|
+
|
70
|
+
@auto_validate_options = {
|
71
|
+
:not_null=>NOT_NULL_OPTIONS,
|
72
|
+
:explicit_not_null=>EXPLICIT_NOT_NULL_OPTIONS,
|
73
|
+
:max_length=>MAX_LENGTH_OPTIONS,
|
74
|
+
:schema_types=>SCHEMA_TYPES_OPTIONS,
|
75
|
+
:unique=>UNIQUE_OPTIONS
|
76
|
+
}.freeze
|
56
77
|
end
|
57
78
|
end
|
58
79
|
|
@@ -63,6 +84,14 @@ module Sequel
|
|
63
84
|
if opts[:not_null] == :presence
|
64
85
|
@auto_validate_presence = true
|
65
86
|
end
|
87
|
+
|
88
|
+
h = @auto_validate_options.dup
|
89
|
+
[:not_null, :explicit_not_null, :max_length, :schema_types, :unique].each do |type|
|
90
|
+
if type_opts = opts[:"#{type}_opts"]
|
91
|
+
h[type] = h[type].merge(type_opts).freeze
|
92
|
+
end
|
93
|
+
end
|
94
|
+
@auto_validate_options = h.freeze
|
66
95
|
end
|
67
96
|
end
|
68
97
|
|
@@ -80,7 +109,10 @@ module Sequel
|
|
80
109
|
# The columns or sets of columns with automatic unique validations
|
81
110
|
attr_reader :auto_validate_unique_columns
|
82
111
|
|
83
|
-
|
112
|
+
# Inherited options
|
113
|
+
attr_reader :auto_validate_options
|
114
|
+
|
115
|
+
Plugins.inherited_instance_variables(self, :@auto_validate_presence=>nil, :@auto_validate_types=>nil, :@auto_validate_not_null_columns=>:dup, :@auto_validate_explicit_not_null_columns=>:dup, :@auto_validate_max_length_columns=>:dup, :@auto_validate_unique_columns=>:dup, :@auto_validate_options => :dup)
|
84
116
|
Plugins.after_set_dataset(self, :setup_auto_validations)
|
85
117
|
|
86
118
|
# Whether to use a presence validation for not null columns
|
@@ -116,7 +148,7 @@ module Sequel
|
|
116
148
|
@auto_validate_max_length_columns = db_schema.select{|col, sch| sch[:type] == :string && sch[:max_length].is_a?(Integer)}.map{|col, sch| [col, sch[:max_length]]}
|
117
149
|
table = dataset.first_source_table
|
118
150
|
@auto_validate_unique_columns = if db.supports_index_parsing? && [Symbol, SQL::QualifiedIdentifier, SQL::Identifier, String].any?{|c| table.is_a?(c)}
|
119
|
-
db.indexes(table).select{|name, idx| idx[:unique] == true}.map{|name, idx| idx[:columns]}
|
151
|
+
db.indexes(table).select{|name, idx| idx[:unique] == true}.map{|name, idx| idx[:columns].length == 1 ? idx[:columns].first : idx[:columns]}
|
120
152
|
else
|
121
153
|
[]
|
122
154
|
end
|
@@ -127,29 +159,31 @@ module Sequel
|
|
127
159
|
# Validate the model's auto validations columns
|
128
160
|
def validate
|
129
161
|
super
|
162
|
+
opts = model.auto_validate_options
|
163
|
+
|
130
164
|
unless (not_null_columns = model.auto_validate_not_null_columns).empty?
|
131
165
|
if model.auto_validate_presence?
|
132
|
-
validates_presence(not_null_columns)
|
166
|
+
validates_presence(not_null_columns, opts[:not_null])
|
133
167
|
else
|
134
|
-
validates_not_null(not_null_columns)
|
168
|
+
validates_not_null(not_null_columns, opts[:not_null])
|
135
169
|
end
|
136
170
|
end
|
137
171
|
unless (not_null_columns = model.auto_validate_explicit_not_null_columns).empty?
|
138
172
|
if model.auto_validate_presence?
|
139
|
-
validates_presence(not_null_columns, :
|
173
|
+
validates_presence(not_null_columns, opts[:explicit_not_null])
|
140
174
|
else
|
141
|
-
validates_not_null(not_null_columns, :
|
175
|
+
validates_not_null(not_null_columns, opts[:explicit_not_null])
|
142
176
|
end
|
143
177
|
end
|
144
178
|
unless (max_length_columns = model.auto_validate_max_length_columns).empty?
|
145
179
|
max_length_columns.each do |col, len|
|
146
|
-
validates_max_length(len, col, :
|
180
|
+
validates_max_length(len, col, opts[:max_length])
|
147
181
|
end
|
148
182
|
end
|
149
183
|
|
150
|
-
validates_schema_types if model.auto_validate_types?
|
184
|
+
validates_schema_types(keys, opts[:schema_types]) if model.auto_validate_types?
|
151
185
|
|
152
|
-
unique_opts =
|
186
|
+
unique_opts = Hash[opts[:unique]]
|
153
187
|
if model.respond_to?(:sti_dataset)
|
154
188
|
unique_opts[:dataset] = model.sti_dataset
|
155
189
|
end
|
@@ -1,15 +1,23 @@
|
|
1
1
|
module Sequel
|
2
2
|
module Plugins
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
3
|
+
# = Overview
|
4
|
+
#
|
5
|
+
# The class_table_inheritance plugin uses the single_table_inheritance
|
6
|
+
# plugin, so it supports all of the single_table_inheritance features, but it
|
7
|
+
# additionally supports subclasses that have additional columns,
|
8
|
+
# which are stored in a separate table with a key referencing the primary table.
|
9
|
+
#
|
10
|
+
# = Detail
|
11
|
+
#
|
12
|
+
# For example, with this hierarchy:
|
7
13
|
#
|
8
14
|
# Employee
|
9
|
-
# / \
|
15
|
+
# / \
|
10
16
|
# Staff Manager
|
17
|
+
# | |
|
18
|
+
# Cook Executive
|
11
19
|
# |
|
12
|
-
#
|
20
|
+
# CEO
|
13
21
|
#
|
14
22
|
# the following database schema may be used (table - columns):
|
15
23
|
#
|
@@ -18,46 +26,50 @@ module Sequel
|
|
18
26
|
# managers :: id, num_staff
|
19
27
|
# executives :: id, num_managers
|
20
28
|
#
|
21
|
-
# The class_table_inheritance plugin assumes that the
|
22
|
-
# (e.g. employees) has a primary key
|
29
|
+
# The class_table_inheritance plugin assumes that the root table
|
30
|
+
# (e.g. employees) has a primary key column (usually autoincrementing),
|
23
31
|
# and all other tables have a foreign key of the same name that points
|
24
|
-
# to the same
|
32
|
+
# to the same column in their superclass's table. In this example,
|
33
|
+
# the employees id column is a primary key and the id column in every
|
34
|
+
# other table is a foreign key referencing the employees id.
|
25
35
|
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
# managers.id :: foreign key referencing employees(id)
|
29
|
-
# executives.id :: foreign key referencing managers(id)
|
36
|
+
# In this example the staff table also stores Cook model objects and the
|
37
|
+
# executives table also stores CEO model objects.
|
30
38
|
#
|
31
|
-
# When using the class_table_inheritance plugin, subclasses
|
32
|
-
# datasets:
|
39
|
+
# When using the class_table_inheritance plugin, subclasses that have additional
|
40
|
+
# columns use joined datasets:
|
33
41
|
#
|
34
42
|
# Employee.dataset.sql
|
35
|
-
# # SELECT
|
36
|
-
# # FROM employees
|
43
|
+
# # SELECT * FROM employees
|
37
44
|
#
|
38
45
|
# Manager.dataset.sql
|
39
|
-
# # SELECT employees.id, employees.name, employees.kind,
|
46
|
+
# # SELECT employees.id, employees.name, employees.kind,
|
47
|
+
# # managers.num_staff
|
40
48
|
# # FROM employees
|
41
49
|
# # JOIN managers ON (managers.id = employees.id)
|
42
50
|
#
|
43
|
-
#
|
44
|
-
# # SELECT employees.id, employees.name, employees.kind,
|
51
|
+
# CEO.dataset.sql
|
52
|
+
# # SELECT employees.id, employees.name, employees.kind,
|
53
|
+
# # managers.num_staff, executives.num_managers
|
45
54
|
# # FROM employees
|
46
55
|
# # JOIN managers ON (managers.id = employees.id)
|
47
56
|
# # JOIN executives ON (executives.id = managers.id)
|
57
|
+
# # WHERE (employees.kind IN ('CEO'))
|
48
58
|
#
|
49
|
-
# This allows
|
59
|
+
# This allows CEO.all to return instances with all attributes
|
50
60
|
# loaded. The plugin overrides the deleting, inserting, and updating
|
51
61
|
# in the model to work with multiple tables, by handling each table
|
52
62
|
# individually.
|
53
63
|
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
# to the
|
64
|
+
# = Subclass loading
|
65
|
+
#
|
66
|
+
# When model objects are retrieved for a superclass the result can contain
|
67
|
+
# subclass instances that only have column entries for the columns in the
|
68
|
+
# superclass table. Calling the column method on the subclass instance for
|
69
|
+
# a column not in the superclass table will cause a query to the database
|
70
|
+
# to get the value for that column. If the subclass instance was retreived
|
71
|
+
# using Dataset#all, the query to the database will attempt to load the column
|
72
|
+
# values for all subclass instances that were retrieved. For example:
|
61
73
|
#
|
62
74
|
# a = Employee.all # [<#Staff>, <#Manager>, <#Executive>]
|
63
75
|
# a.first.values # {:id=>1, name=>'S', :kind=>'Staff'}
|
@@ -67,171 +79,208 @@ module Sequel
|
|
67
79
|
# via the superclass, call Model#refresh.
|
68
80
|
#
|
69
81
|
# a = Employee.first
|
70
|
-
# a.values # {:id=>1, name=>'S', :kind=>'
|
82
|
+
# a.values # {:id=>1, name=>'S', :kind=>'CEO'}
|
71
83
|
# a.refresh.values # {:id=>1, name=>'S', :kind=>'Executive', :num_staff=>4, :num_managers=>2}
|
72
|
-
#
|
73
|
-
# Usage:
|
74
84
|
#
|
75
|
-
#
|
76
|
-
#
|
85
|
+
# = Usage
|
86
|
+
#
|
87
|
+
# # Use the default of storing the class name in the sti_key
|
88
|
+
# # column (:kind in this case)
|
77
89
|
# class Employee < Sequel::Model
|
78
|
-
# plugin :class_table_inheritance
|
90
|
+
# plugin :class_table_inheritance, :key=>:kind
|
79
91
|
# end
|
80
92
|
#
|
81
93
|
# # Have subclasses inherit from the appropriate class
|
82
|
-
# class Staff < Employee; end
|
83
|
-
# class
|
84
|
-
# class
|
94
|
+
# class Staff < Employee; end # uses staff table
|
95
|
+
# class Cook < Staff; end # cooks table doesn't exist so uses staff table
|
96
|
+
# class Manager < Employee; end # uses managers table
|
97
|
+
# class Executive < Manager; end # uses executives table
|
98
|
+
# class CEO < Executive; end # ceos table doesn't exist so uses executives table
|
99
|
+
#
|
100
|
+
# # Some examples of using these options:
|
101
|
+
#
|
102
|
+
# # Specifying the tables with a :table_map hash
|
103
|
+
# Employee.plugin :class_table_inheritance,
|
104
|
+
# :table_map=>{:Employee => :employees,
|
105
|
+
# :Staff => :staff,
|
106
|
+
# :Cook => :staff,
|
107
|
+
# :Manager => :managers,
|
108
|
+
# :Executive => :executives,
|
109
|
+
# :CEO => :executives }
|
110
|
+
#
|
111
|
+
# # Using integers to store the class type, with a :model_map hash
|
112
|
+
# # and an sti_key of :type
|
113
|
+
# Employee.plugin :class_table_inheritance, :type,
|
114
|
+
# :model_map=>{1=>:Staff, 2=>:Cook, 3=>:Manager, 4=>:Executive, 5=>:CEO}
|
85
115
|
#
|
86
|
-
# #
|
87
|
-
#
|
88
|
-
#
|
89
|
-
#
|
90
|
-
#
|
91
|
-
#
|
116
|
+
# # Using non-class name strings
|
117
|
+
# Employee.plugin :class_table_inheritance, :key=>:type,
|
118
|
+
# :model_map=>{'staff'=>:Staff, 'cook staff'=>:Cook, 'supervisor'=>:Manager}
|
119
|
+
#
|
120
|
+
# # By default the plugin sets the respective column value
|
121
|
+
# # when a new instance is created.
|
122
|
+
# Cook.create.type == 'cook staff'
|
123
|
+
# Manager.create.type == 'supervisor'
|
124
|
+
#
|
125
|
+
# # You can customize this behavior with the :key_chooser option.
|
126
|
+
# # This is most useful when using a non-bijective mapping.
|
127
|
+
# Employee.plugin :class_table_inheritance, :key=>:type,
|
128
|
+
# :model_map=>{'cook staff'=>:Cook, 'supervisor'=>:Manager},
|
129
|
+
# :key_chooser=>proc{|instance| instance.model.sti_key_map[instance.model.to_s].first || 'stranger' }
|
130
|
+
#
|
131
|
+
# # Using custom procs, with :model_map taking column values
|
132
|
+
# # and yielding either a class, string, symbol, or nil,
|
133
|
+
# # and :key_map taking a class object and returning the column
|
134
|
+
# # value to use
|
135
|
+
# Employee.plugin :single_table_inheritance, :key=>:type,
|
136
|
+
# :model_map=>proc{|v| v.reverse},
|
137
|
+
# :key_map=>proc{|klass| klass.name.reverse}
|
138
|
+
#
|
139
|
+
# # You can use the same class for multiple values.
|
140
|
+
# # This is mainly useful when the sti_key column contains multiple values
|
141
|
+
# # which are different but do not require different code.
|
142
|
+
# Employee.plugin :single_table_inheritance, :key=>:type,
|
143
|
+
# :model_map=>{'staff' => "Staff",
|
144
|
+
# 'manager' => "Manager",
|
145
|
+
# 'overpayed staff' => "Staff",
|
146
|
+
# 'underpayed staff' => "Staff"}
|
147
|
+
#
|
148
|
+
# One minor issue to note is that if you specify the <tt>:key_map</tt>
|
149
|
+
# option as a hash, instead of having it inferred from the <tt>:model_map</tt>,
|
150
|
+
# you should only use class name strings as keys, you should not use symbols
|
151
|
+
# as keys.
|
92
152
|
module ClassTableInheritance
|
93
|
-
# The class_table_inheritance plugin requires the
|
94
|
-
# to handle lazily-loaded attributes
|
95
|
-
# by superclass methods.
|
96
|
-
def self.apply(model, opts=OPTS)
|
153
|
+
# The class_table_inheritance plugin requires the single_table_inheritance
|
154
|
+
# plugin and the lazy_attributes plugin to handle lazily-loaded attributes
|
155
|
+
# for subclass instances returned by superclass methods.
|
156
|
+
def self.apply(model, opts = OPTS)
|
157
|
+
model.plugin :single_table_inheritance, nil
|
97
158
|
model.plugin :lazy_attributes
|
98
159
|
end
|
99
|
-
|
100
|
-
# Initialize the
|
101
|
-
#
|
102
|
-
#
|
103
|
-
#
|
104
|
-
#
|
105
|
-
#
|
106
|
-
#
|
107
|
-
#
|
108
|
-
#
|
109
|
-
#
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
# manually (usually in a before_create hook).
|
114
|
-
def self.configure(model, opts=OPTS)
|
160
|
+
|
161
|
+
# Initialize the plugin using the following options:
|
162
|
+
# :key :: Column symbol that holds the key that identifies the class to use.
|
163
|
+
# Necessary if you want to call model methods on a superclass
|
164
|
+
# that return subclass instances
|
165
|
+
# :model_map :: Hash or proc mapping the key column values to model class names.
|
166
|
+
# :key_map :: Hash or proc mapping model class names to key column values.
|
167
|
+
# Each value or return is an array of possible key column values.
|
168
|
+
# :key_chooser :: proc returning key for the provided model instance
|
169
|
+
# :table_map :: Hash with class name symbols keys mapping to table name symbol values
|
170
|
+
# Overrides implicit table names
|
171
|
+
def self.configure(model, opts = OPTS)
|
172
|
+
SingleTableInheritance.configure model, opts[:key], opts
|
173
|
+
|
115
174
|
model.instance_eval do
|
116
|
-
@
|
117
|
-
@cti_key = opts[:key]
|
175
|
+
@cti_models = [self]
|
118
176
|
@cti_tables = [table_name]
|
119
|
-
@
|
177
|
+
@cti_instance_dataset = @instance_dataset
|
178
|
+
@cti_table_columns = columns
|
120
179
|
@cti_table_map = opts[:table_map] || {}
|
121
|
-
@cti_model_map = opts[:model_map]
|
122
|
-
set_dataset_cti_row_proc
|
123
|
-
set_dataset(dataset.select(*columns.map{|c| Sequel.qualify(table_name, Sequel.identifier(c))}))
|
124
180
|
end
|
125
181
|
end
|
126
182
|
|
127
183
|
module ClassMethods
|
184
|
+
# An array of each model in the inheritance hierarchy that uses an
|
185
|
+
# backed by a new table.
|
186
|
+
attr_reader :cti_models
|
187
|
+
|
128
188
|
# The parent/root/base model for this class table inheritance hierarchy.
|
129
|
-
# This is the only model in the hierarchy that
|
130
|
-
# class_table_inheritance plugin.
|
131
|
-
|
132
|
-
|
133
|
-
|
189
|
+
# This is the only model in the hierarchy that loads the
|
190
|
+
# class_table_inheritance plugin. For backwards compatibility.
|
191
|
+
def cti_base_model
|
192
|
+
@cti_models.first
|
193
|
+
end
|
194
|
+
|
195
|
+
# An array of column symbols for the backing database table,
|
134
196
|
# giving the columns to update in each backing database table.
|
135
|
-
attr_reader :
|
136
|
-
|
137
|
-
# The
|
138
|
-
#
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
# A hash with keys being values of the cti_key column, and values
|
143
|
-
# being class name strings or symbols. Used if you don't want to
|
144
|
-
# store class names in the database.
|
145
|
-
attr_reader :cti_model_map
|
146
|
-
|
197
|
+
attr_reader :cti_table_columns
|
198
|
+
|
199
|
+
# The dataset that table instance datasets are based on.
|
200
|
+
# Used for database modifications
|
201
|
+
attr_reader :cti_instance_dataset
|
202
|
+
|
147
203
|
# An array of table symbols that back this model. The first is
|
148
204
|
# cti_base_model table symbol, and the last is the current model
|
149
205
|
# table symbol.
|
150
206
|
attr_reader :cti_tables
|
151
|
-
|
207
|
+
|
152
208
|
# A hash with class name symbol keys and table name symbol values.
|
153
209
|
# Specified with the :table_map option to the plugin, and used if
|
154
210
|
# the implicit naming is incorrect.
|
155
211
|
attr_reader :cti_table_map
|
156
212
|
|
157
|
-
|
213
|
+
# Hash with table name symbol keys and arrays of column symbol values,
|
214
|
+
# giving the columns to update in each backing database table.
|
215
|
+
# For backwards compatibility.
|
216
|
+
def cti_columns
|
217
|
+
h = {}
|
218
|
+
cti_models.each { |m| h[m.table_name] = m.cti_table_columns }
|
219
|
+
h
|
220
|
+
end
|
221
|
+
|
222
|
+
# Alias to sti_key, for backwards compatibility.
|
223
|
+
def cti_key; sti_key; end
|
224
|
+
|
225
|
+
# Alias to sti_model_map, for backwards compatibility.
|
226
|
+
def cti_model_map; sti_model_map; end
|
227
|
+
|
228
|
+
Plugins.inherited_instance_variables(self, :@cti_models=>nil, :@cti_tables=>nil, :@cti_table_columns=>nil, :@cti_instance_dataset=>nil, :@cti_table_map=>nil)
|
158
229
|
|
159
|
-
# Add the appropriate data structures to the subclass. Does not
|
160
|
-
# allow anonymous subclasses to be created, since they would not
|
161
|
-
# be mappable to a table.
|
162
230
|
def inherited(subclass)
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
231
|
+
ds = sti_dataset
|
232
|
+
|
233
|
+
# Prevent inherited in model/base.rb from setting the dataset
|
234
|
+
subclass.instance_eval { @dataset = nil }
|
235
|
+
|
236
|
+
super
|
237
|
+
|
238
|
+
# Set table if this is a class table inheritance
|
169
239
|
table = nil
|
170
240
|
columns = nil
|
171
|
-
subclass.
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
# the main column accessor module is included in the class before any
|
180
|
-
# plugin accessor modules (such as the lazy attributes accessor module).
|
181
|
-
set_dataset(ds.join(table, pk=>pk).select_append(*(columns - [primary_key]).map{|c| Sequel.qualify(table, Sequel.identifier(c))}))
|
182
|
-
set_columns(self.columns)
|
241
|
+
if (n = subclass.name) && !n.empty?
|
242
|
+
if table = cti_table_map[n.to_sym]
|
243
|
+
columns = db.from(table).columns
|
244
|
+
else
|
245
|
+
table = subclass.implicit_table_name
|
246
|
+
columns = db.from(table).columns rescue nil
|
247
|
+
table = nil if !columns || columns.empty?
|
248
|
+
end
|
183
249
|
end
|
184
|
-
|
250
|
+
table = nil if table && (table == table_name)
|
251
|
+
|
252
|
+
return unless table
|
253
|
+
|
254
|
+
pk = primary_key
|
185
255
|
subclass.instance_eval do
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
256
|
+
if cti_tables.length == 1
|
257
|
+
ds = ds.select(*self.columns.map{|cc| Sequel.qualify(table_name, Sequel.identifier(cc))})
|
258
|
+
end
|
259
|
+
sel_app = (columns - [pk]).map{|cc| Sequel.qualify(table, Sequel.identifier(cc))}
|
260
|
+
@sti_dataset = ds.join(table, pk=>pk).select_append(*sel_app)
|
261
|
+
set_dataset(@sti_dataset)
|
262
|
+
set_columns(self.columns)
|
263
|
+
dataset.row_proc = lambda{|r| subclass.sti_load(r)}
|
264
|
+
(columns - [pk]).each{|a| define_lazy_attribute_getter(a, :dataset=>dataset, :table=>table)}
|
265
|
+
|
266
|
+
@cti_models += [self]
|
267
|
+
@cti_tables += [table]
|
268
|
+
@cti_table_columns = columns
|
269
|
+
@cti_instance_dataset = db.from(table)
|
270
|
+
|
271
|
+
cti_tables.reverse_each do |ct|
|
272
|
+
db.schema(ct).each{|sk,v| db_schema[sk] = v}
|
190
273
|
end
|
191
274
|
end
|
192
275
|
end
|
193
|
-
|
194
|
-
# The primary key in the parent/base/root model, which should have a
|
195
|
-
# foreign key with the same name referencing it in each model subclass.
|
196
|
-
def primary_key
|
197
|
-
return super if self == cti_base_model
|
198
|
-
cti_base_model.primary_key
|
199
|
-
end
|
200
|
-
|
201
|
-
# The table name for the current model class's main table (not used
|
202
|
-
# by any superclasses).
|
203
|
-
def table_name
|
204
|
-
self == cti_base_model ? super : cti_tables.last
|
205
|
-
end
|
206
276
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
# row proc to one that handles inheritance correctly.
|
211
|
-
def set_dataset_row_proc(ds)
|
212
|
-
ds.row_proc = @dataset.row_proc if @dataset
|
277
|
+
# The table name for the current model class's main table.
|
278
|
+
def table_name
|
279
|
+
cti_tables ? cti_tables.last : super
|
213
280
|
end
|
214
281
|
|
215
|
-
|
216
|
-
|
217
|
-
def set_dataset_cti_row_proc
|
218
|
-
m = method(:constantize)
|
219
|
-
dataset.row_proc = if ck = cti_key
|
220
|
-
if model_map = cti_model_map
|
221
|
-
lambda do |r|
|
222
|
-
mod = if name = model_map[r[ck]]
|
223
|
-
m.call(name)
|
224
|
-
else
|
225
|
-
self
|
226
|
-
end
|
227
|
-
mod.call(r)
|
228
|
-
end
|
229
|
-
else
|
230
|
-
lambda{|r| (m.call(r[ck]) rescue self).call(r)}
|
231
|
-
end
|
232
|
-
else
|
233
|
-
self
|
234
|
-
end
|
282
|
+
def sti_class_from_key(key)
|
283
|
+
sti_class(sti_model_map[key])
|
235
284
|
end
|
236
285
|
end
|
237
286
|
|
@@ -240,47 +289,55 @@ module Sequel
|
|
240
289
|
# most recent table and going through all superclasses.
|
241
290
|
def delete
|
242
291
|
raise Sequel::Error, "can't delete frozen object" if frozen?
|
243
|
-
|
244
|
-
|
245
|
-
m.db.from(table).filter(m.primary_key=>pk).delete
|
292
|
+
model.cti_models.reverse_each do |m|
|
293
|
+
cti_this(m).delete
|
246
294
|
end
|
247
295
|
self
|
248
296
|
end
|
249
|
-
|
297
|
+
|
250
298
|
private
|
251
|
-
|
252
|
-
|
299
|
+
|
300
|
+
def cti_this(model)
|
301
|
+
use_server(model.cti_instance_dataset.filter(model.primary_key_hash(pk)))
|
302
|
+
end
|
303
|
+
|
304
|
+
# Set the sti_key column based on the sti_key_map.
|
253
305
|
def _before_validation
|
254
|
-
if new? &&
|
255
|
-
|
306
|
+
if new? && (set = self[model.sti_key])
|
307
|
+
exp = model.sti_key_chooser.call(self)
|
308
|
+
if set != exp
|
309
|
+
set_table = model.sti_class_from_key(set).table_name
|
310
|
+
exp_table = model.sti_class_from_key(exp).table_name
|
311
|
+
set_column_value("#{model.sti_key}=", exp) if set_table != exp_table
|
312
|
+
end
|
256
313
|
end
|
257
314
|
super
|
258
315
|
end
|
259
|
-
|
316
|
+
|
260
317
|
# Insert rows into all backing tables, using the columns
|
261
|
-
# in each table.
|
318
|
+
# in each table.
|
262
319
|
def _insert
|
263
|
-
return super if model ==
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
320
|
+
return super if model.cti_tables.length == 1
|
321
|
+
model.cti_models.each do |m|
|
322
|
+
v = {}
|
323
|
+
m.cti_table_columns.each{|c| v[c] = @values[c] if @values.include?(c)}
|
324
|
+
ds = use_server(m.cti_instance_dataset)
|
325
|
+
if ds.supports_insert_select? && (h = ds.insert_select(v))
|
326
|
+
@values.merge!(h)
|
327
|
+
else
|
328
|
+
nid = ds.insert(v)
|
329
|
+
@values[primary_key] ||= nid
|
330
|
+
end
|
272
331
|
end
|
273
|
-
@values[primary_key]
|
332
|
+
db.dataset.supports_insert_select? ? nil : @values[primary_key]
|
274
333
|
end
|
275
|
-
|
334
|
+
|
276
335
|
# Update rows in all backing tables, using the columns in each table.
|
277
336
|
def _update(columns)
|
278
|
-
|
279
|
-
m = model
|
280
|
-
m.cti_tables.each do |table|
|
337
|
+
model.cti_models.each do |m|
|
281
338
|
h = {}
|
282
|
-
m.
|
283
|
-
m
|
339
|
+
m.cti_table_columns.each{|c| h[c] = columns[c] if columns.include?(c)}
|
340
|
+
cti_this(m).update(h) unless h.empty?
|
284
341
|
end
|
285
342
|
end
|
286
343
|
end
|