sequel 4.23.0 → 4.24.0

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