sequel 4.23.0 → 4.24.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 +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
|