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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +26 -0
  3. data/Rakefile +1 -1
  4. data/doc/release_notes/4.24.0.txt +99 -0
  5. data/doc/sql.rdoc +10 -1
  6. data/lib/sequel/adapters/jdbc.rb +7 -0
  7. data/lib/sequel/adapters/jdbc/cubrid.rb +1 -1
  8. data/lib/sequel/adapters/jdbc/db2.rb +1 -1
  9. data/lib/sequel/adapters/jdbc/derby.rb +1 -1
  10. data/lib/sequel/adapters/jdbc/h2.rb +1 -1
  11. data/lib/sequel/adapters/jdbc/hsqldb.rb +1 -1
  12. data/lib/sequel/adapters/jdbc/mssql.rb +1 -1
  13. data/lib/sequel/adapters/jdbc/mysql.rb +2 -2
  14. data/lib/sequel/adapters/jdbc/oracle.rb +1 -1
  15. data/lib/sequel/adapters/jdbc/sqlanywhere.rb +1 -1
  16. data/lib/sequel/adapters/jdbc/sqlite.rb +1 -1
  17. data/lib/sequel/adapters/postgres.rb +14 -6
  18. data/lib/sequel/adapters/shared/mssql.rb +1 -1
  19. data/lib/sequel/core.rb +12 -1
  20. data/lib/sequel/database/connecting.rb +1 -2
  21. data/lib/sequel/extensions/pg_inet_ops.rb +200 -0
  22. data/lib/sequel/plugins/association_pks.rb +63 -18
  23. data/lib/sequel/plugins/auto_validations.rb +43 -9
  24. data/lib/sequel/plugins/class_table_inheritance.rb +236 -179
  25. data/lib/sequel/plugins/update_refresh.rb +26 -1
  26. data/lib/sequel/plugins/validation_helpers.rb +7 -2
  27. data/lib/sequel/version.rb +1 -1
  28. data/spec/adapters/oracle_spec.rb +1 -1
  29. data/spec/adapters/postgres_spec.rb +61 -0
  30. data/spec/core_extensions_spec.rb +5 -1
  31. data/spec/extensions/association_pks_spec.rb +73 -1
  32. data/spec/extensions/auto_validations_spec.rb +34 -0
  33. data/spec/extensions/class_table_inheritance_spec.rb +58 -54
  34. data/spec/extensions/pg_inet_ops_spec.rb +101 -0
  35. data/spec/extensions/spec_helper.rb +5 -5
  36. data/spec/extensions/update_refresh_spec.rb +12 -0
  37. data/spec/extensions/validation_helpers_spec.rb +7 -0
  38. data/spec/integration/plugin_test.rb +48 -13
  39. metadata +6 -4
  40. data/lib/sequel/adapters/db2.rb +0 -229
  41. 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 the association_pks and association_pks=
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 the associated objects to set the primary
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 def_association_pks_getter(opts, &block)
39
- association_module_def(:"#{singularize(opts[:name])}_pks", opts, &block)
40
- end
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 ever method call.
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
- def_association_pks_getter(opts) do
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
- def_association_pks_getter(opts) do
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
- def_association_pks_setter(opts) do |pks|
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
- def_association_pks_getter(opts) do
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
- def_association_pks_setter(opts) do |pks|
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
- 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)
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, :allow_missing=>true)
173
+ validates_presence(not_null_columns, opts[:explicit_not_null])
140
174
  else
141
- validates_not_null(not_null_columns, :allow_missing=>true)
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, :allow_nil=>true)
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
- # The class_table_inheritance plugin allows you to model inheritance in the
4
- # database using a table per model class in the hierarchy, with only columns
5
- # unique to that model class (or subclass hierarchy) being stored in the related
6
- # table. For example, with this hierarchy:
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
- # Executive
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 main table
22
- # (e.g. employees) has a primary key field (usually autoincrementing),
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 key in their superclass's table. For example:
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
- # employees.id :: primary key, autoincrementing
27
- # staff.id :: foreign key referencing employees(id)
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 use joined
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 employees.id, employees.name, employees.kind
36
- # # FROM employees
43
+ # # SELECT * FROM employees
37
44
  #
38
45
  # Manager.dataset.sql
39
- # # SELECT employees.id, employees.name, employees.kind, managers.num_staff
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
- # Executive.dataset.sql
44
- # # SELECT employees.id, employees.name, employees.kind, managers.num_staff, executives.num_managers
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 Executive.all to return instances with all attributes
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
- # This plugin allows the use of a :key option when loading to mark
55
- # a column holding a class name. This allows methods on the
56
- # superclass to return instances of specific subclasses.
57
- # This plugin also requires the lazy_attributes plugin and uses it to
58
- # return subclass specific attributes that would not be loaded
59
- # when calling superclass methods (since those wouldn't join
60
- # to the subclass tables). For example:
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=>'Executive'}
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
- # # Set up class table inheritance in the parent class
76
- # # (Not in the subclasses)
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 Manager < Employee; end
84
- # class Executive < Manager; end
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
- # # You can also set options when loading the plugin:
87
- # # :kind :: column to hold the class name
88
- # # :table_map :: map of class name symbols to table name symbols
89
- # # :model_map :: map of column values to class name symbols
90
- # Employee.plugin :class_table_inheritance, :key=>:kind, :table_map=>{:Staff=>:staff},
91
- # :model_map=>{1=>:Employee, 2=>:Manager, 3=>:Executive, 4=>:Staff}
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 lazy_attributes plugin
94
- # to handle lazily-loaded attributes for subclass instances returned
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 per-model data structures and set the dataset's row_proc
101
- # to check for the :key option column for the type of class when loading objects.
102
- # Options:
103
- # :key :: The column symbol holding the name of the model class this
104
- # is an instance of. Necessary if you want to call model methods
105
- # using the superclass, but have them return subclass instances.
106
- # :table_map :: Hash with class name symbol keys and table name symbol
107
- # values. Necessary if the implicit table name for the model class
108
- # does not match the database table name
109
- # :model_map :: Hash with keys being values of the cti_key column, and values
110
- # being class name strings or symbols. Used if you don't want to
111
- # store class names in the database. If you use this option, you
112
- # are responsible for setting the values of the cti_key column
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
- @cti_base_model = self
117
- @cti_key = opts[:key]
175
+ @cti_models = [self]
118
176
  @cti_tables = [table_name]
119
- @cti_columns = {table_name=>columns}
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 load the
130
- # class_table_inheritance plugin.
131
- attr_reader :cti_base_model
132
-
133
- # Hash with table name symbol keys and arrays of column symbol values,
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 :cti_columns
136
-
137
- # The column containing the class name as a string. Used to
138
- # return instances of subclasses when calling the superclass's
139
- # load method.
140
- attr_reader :cti_key
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
- Plugins.inherited_instance_variables(self, :@cti_key=>nil, :@cti_model_map=>nil, :@cti_table_map=>nil)
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
- cc = cti_columns
164
- ct = cti_tables.dup
165
- ctm = cti_table_map
166
- cbm = cti_base_model
167
- pk = primary_key
168
- ds = dataset
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.instance_eval do
172
- raise(Error, "cannot create anonymous subclass for model class using class_table_inheritance") if !(n = name) || n.empty?
173
- table = ctm[n.to_sym] || implicit_table_name
174
- columns = db.from(table).columns
175
- @cti_tables = ct + [table]
176
- @cti_columns = cc.merge(table=>columns)
177
- @cti_base_model = cbm
178
- # Need to set dataset and columns before calling super so that
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
- super
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
- set_dataset_cti_row_proc
187
- (columns - [cbm.primary_key]).each{|a| define_lazy_attribute_getter(a, :dataset=>dataset, :table=>table)}
188
- cti_tables.reverse.each do |t|
189
- db.schema(t).each{|k,v| db_schema[k] = v}
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
- private
208
-
209
- # If calling set_dataset manually, make sure to set the dataset
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
- # Set the row_proc for the model's dataset appropriately
216
- # based on the cti key and model map.
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
- m = model
244
- m.cti_tables.reverse.each do |table|
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
- # Set the cti_key column to the name of the model.
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? && model.cti_key && !model.cti_model_map
255
- set_column_value("#{model.cti_key}=", model.name.to_s)
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 == model.cti_base_model
264
- iid = @values[primary_key]
265
- m = model
266
- m.cti_tables.each do |table|
267
- h = {}
268
- h[m.primary_key] ||= iid if iid
269
- m.cti_columns[table].each{|c| h[c] = @values[c] if @values.include?(c)}
270
- nid = m.db.from(table).insert(h)
271
- iid ||= nid
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] = iid
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
- pkh = pk_hash
279
- m = model
280
- m.cti_tables.each do |table|
337
+ model.cti_models.each do |m|
281
338
  h = {}
282
- m.cti_columns[table].each{|c| h[c] = columns[c] if columns.include?(c)}
283
- m.db.from(table).filter(pkh).update(h) unless h.empty?
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