sequel 4.1.1 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +32 -0
  3. data/doc/opening_databases.rdoc +4 -0
  4. data/doc/release_notes/4.2.0.txt +129 -0
  5. data/lib/sequel/adapters/jdbc/hsqldb.rb +5 -0
  6. data/lib/sequel/adapters/mysql2.rb +2 -1
  7. data/lib/sequel/adapters/postgres.rb +8 -4
  8. data/lib/sequel/adapters/shared/db2.rb +5 -0
  9. data/lib/sequel/adapters/shared/mssql.rb +15 -4
  10. data/lib/sequel/adapters/shared/mysql.rb +1 -0
  11. data/lib/sequel/adapters/shared/oracle.rb +1 -1
  12. data/lib/sequel/adapters/shared/postgres.rb +10 -0
  13. data/lib/sequel/adapters/shared/sqlite.rb +5 -0
  14. data/lib/sequel/database/features.rb +6 -1
  15. data/lib/sequel/database/schema_methods.rb +3 -7
  16. data/lib/sequel/dataset/actions.rb +3 -4
  17. data/lib/sequel/dataset/features.rb +5 -0
  18. data/lib/sequel/dataset/misc.rb +28 -3
  19. data/lib/sequel/dataset/mutation.rb +37 -11
  20. data/lib/sequel/dataset/prepared_statements.rb +1 -3
  21. data/lib/sequel/dataset/query.rb +12 -3
  22. data/lib/sequel/dataset/sql.rb +12 -6
  23. data/lib/sequel/deprecated.rb +1 -1
  24. data/lib/sequel/extensions/columns_introspection.rb +1 -1
  25. data/lib/sequel/extensions/core_extensions.rb +0 -2
  26. data/lib/sequel/extensions/empty_array_ignore_nulls.rb +1 -1
  27. data/lib/sequel/extensions/filter_having.rb +1 -1
  28. data/lib/sequel/extensions/from_block.rb +31 -0
  29. data/lib/sequel/extensions/graph_each.rb +1 -1
  30. data/lib/sequel/extensions/hash_aliases.rb +1 -1
  31. data/lib/sequel/extensions/mssql_emulate_lateral_with_apply.rb +78 -0
  32. data/lib/sequel/extensions/pagination.rb +1 -1
  33. data/lib/sequel/extensions/pg_loose_count.rb +32 -0
  34. data/lib/sequel/extensions/pg_static_cache_updater.rb +133 -0
  35. data/lib/sequel/extensions/pretty_table.rb +1 -1
  36. data/lib/sequel/extensions/query.rb +3 -1
  37. data/lib/sequel/extensions/query_literals.rb +1 -1
  38. data/lib/sequel/extensions/select_remove.rb +1 -1
  39. data/lib/sequel/extensions/sequel_3_dataset_methods.rb +1 -1
  40. data/lib/sequel/extensions/set_overrides.rb +1 -1
  41. data/lib/sequel/model.rb +1 -1
  42. data/lib/sequel/model/base.rb +20 -6
  43. data/lib/sequel/model/exceptions.rb +1 -1
  44. data/lib/sequel/plugins/composition.rb +9 -0
  45. data/lib/sequel/plugins/dirty.rb +19 -8
  46. data/lib/sequel/plugins/instance_filters.rb +9 -0
  47. data/lib/sequel/plugins/serialization.rb +9 -0
  48. data/lib/sequel/plugins/serialization_modification_detection.rb +9 -0
  49. data/lib/sequel/plugins/static_cache.rb +96 -28
  50. data/lib/sequel/version.rb +2 -2
  51. data/spec/adapters/mssql_spec.rb +1 -1
  52. data/spec/adapters/postgres_spec.rb +70 -0
  53. data/spec/core/dataset_spec.rb +58 -1
  54. data/spec/core/deprecated_spec.rb +1 -1
  55. data/spec/core/schema_spec.rb +18 -0
  56. data/spec/extensions/composition_spec.rb +7 -0
  57. data/spec/extensions/dirty_spec.rb +9 -0
  58. data/spec/extensions/from_block_spec.rb +21 -0
  59. data/spec/extensions/instance_filters_spec.rb +6 -0
  60. data/spec/extensions/pg_loose_count_spec.rb +17 -0
  61. data/spec/extensions/pg_static_cache_updater_spec.rb +80 -0
  62. data/spec/extensions/query_spec.rb +8 -0
  63. data/spec/extensions/serialization_modification_detection_spec.rb +9 -0
  64. data/spec/extensions/serialization_spec.rb +7 -0
  65. data/spec/extensions/set_overrides_spec.rb +12 -0
  66. data/spec/extensions/static_cache_spec.rb +314 -154
  67. data/spec/integration/dataset_test.rb +12 -2
  68. data/spec/integration/schema_test.rb +13 -0
  69. data/spec/model/record_spec.rb +74 -0
  70. metadata +13 -3
@@ -0,0 +1,133 @@
1
+ # The pg_static_cache_updater extension is designed to
2
+ # automatically update the caches in the models using the
3
+ # static_cache plugin when changes to the underlying tables
4
+ # are detected.
5
+ #
6
+ # Before using the extension in production, you have to add
7
+ # triggers to the tables for the classes where you want the
8
+ # caches updated automatically. You would generally do this
9
+ # during a migration:
10
+ #
11
+ # Sequel.migration do
12
+ # up do
13
+ # extension :pg_static_cache_updater
14
+ # create_static_cache_update_function
15
+ # create_static_cache_update_trigger(:table_1)
16
+ # create_static_cache_update_trigger(:table_2)
17
+ # end
18
+ # down do
19
+ # extension :pg_static_cache_updater
20
+ # drop_trigger(:table_2, default_static_cache_update_name)
21
+ # drop_trigger(:table_1, default_static_cache_update_name)
22
+ # drop_function(default_static_cache_update_name)
23
+ # end
24
+ # end
25
+ #
26
+ # After the triggers have been added, in your application process,
27
+ # after setting up your models, you need to listen for changes to
28
+ # the underlying tables:
29
+ #
30
+ # class Model1 < Sequel::Model(:table_1)
31
+ # plugin :static_cache
32
+ # end
33
+ # class Model2 < Sequel::Model(:table_2)
34
+ # plugin :static_cache
35
+ # end
36
+ #
37
+ # DB.extension :pg_static_cache_updater
38
+ # DB.listen_for_static_cache_updates([Model1, Model2])
39
+ #
40
+ # When an INSERT/UPDATE/DELETE happens on the underlying table,
41
+ # the trigger will send a notification with the table's OID.
42
+ # The application(s) listening on that channel will receive
43
+ # the notification, check the oid to see if it matches one
44
+ # for the model tables it is interested in, and tell that model
45
+ # to reload the cache if there is a match.
46
+ #
47
+ # Note that listen_for_static_cache_updates spawns a new thread
48
+ # which will reserve its own database connection. This thread
49
+ # runs until the application process is shutdown.
50
+ #
51
+ # Also note that PostgreSQL does not send notifications to
52
+ # channels until after the transaction including the changes
53
+ # is committed. Also, because a separate thread is used to
54
+ # listen for notifications, there may be a slight delay between
55
+ # when the transaction is committed and when the cache is
56
+ # reloaded.
57
+ #
58
+ # Requirements:
59
+ # * PostgreSQL 9.0+
60
+ # * Listening Database object must be using the postgres adapter
61
+ # with the pg driver (the model classes do not have to
62
+ # use the same Database).
63
+ # * Must be using a thread-safe connection pool (the default).
64
+
65
+ module Sequel
66
+ module Postgres
67
+ module StaticCacheUpdater
68
+ # Add the static cache update function to the PostgreSQL database.
69
+ # This must be added before any triggers using this function are
70
+ # added.
71
+ #
72
+ # Options:
73
+ # :channel_name :: Override the channel name to use.
74
+ # :function_name :: Override the function name to use.
75
+ def create_static_cache_update_function(opts=OPTS)
76
+ create_function(opts[:function_name]||default_static_cache_update_name, <<SQL, :returns=>:trigger, :language=>:plpgsql)
77
+ BEGIN
78
+ PERFORM pg_notify(#{literal((opts[:channel_name]||default_static_cache_update_name).to_s)}, TG_RELID::text);
79
+ RETURN NULL;
80
+ END
81
+ SQL
82
+ end
83
+
84
+ # Add a trigger to the given table that calls the function
85
+ # which will notify about table changes.
86
+ #
87
+ # Options:
88
+ # :function_name :: Override the function name to use.
89
+ # :trigger_name :: Override the trigger name to use.
90
+ def create_static_cache_update_trigger(table, opts=OPTS)
91
+ create_trigger(table, opts[:trigger_name]||default_static_cache_update_name, opts[:function_name]||default_static_cache_update_name, :after=>true)
92
+ end
93
+
94
+ # The default name for the function, trigger, and notification channel
95
+ # for this extension.
96
+ def default_static_cache_update_name
97
+ :sequel_static_cache_update
98
+ end
99
+
100
+ # Listen on the notification channel for changes to any of tables for
101
+ # the models given. If notified about a change to one of the tables,
102
+ # reload the cache for the related model. Options given are also
103
+ # passed to Database#listen.
104
+ #
105
+ # Note that this implementation does not currently support model
106
+ # models that use the same underlying table.
107
+ #
108
+ # Options:
109
+ # :channel_name :: Override the channel name to use.
110
+ def listen_for_static_cache_updates(models, opts=OPTS)
111
+ raise Error, "this database object does not respond to listen, use the postgres adapter with the pg driver" unless respond_to?(:listen)
112
+ models = [models] unless models.is_a?(Array)
113
+ raise Error, "array of models to listen for changes cannot be empty" if models.empty?
114
+
115
+ oid_map = {}
116
+ models.each do |model|
117
+ raise Error, "#{model.inspect} does not use the static_cache plugin" unless model.respond_to?(:load_cache, true)
118
+ oid_map[get(regclass_oid(model.dataset.first_source_table))] = model
119
+ end
120
+
121
+ Thread.new do
122
+ listen(opts[:channel_name]||default_static_cache_update_name, {:loop=>true}.merge(opts)) do |_, _, oid|
123
+ if model = oid_map[oid.to_i]
124
+ model.send(:load_cache)
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ Database.register_extension(:pg_static_cache_updater, Postgres::StaticCacheUpdater)
133
+ end
@@ -12,7 +12,7 @@
12
12
  # You can load this extension into specific datasets:
13
13
  #
14
14
  # ds = DB[:table]
15
- # ds.extension(:pretty_table)
15
+ # ds = ds.extension(:pretty_table)
16
16
  #
17
17
  # Or you can load it into all of a database's datasets, which
18
18
  # is probably the desired behavior if you are using this extension:
@@ -5,7 +5,7 @@
5
5
  # You can load this extension into specific datasets:
6
6
  #
7
7
  # ds = DB[:table]
8
- # ds.extension(:query)
8
+ # ds = ds.extension(:query)
9
9
  #
10
10
  # Or you can load it into all of a database's datasets, which
11
11
  # is probably the desired behavior if you are using this extension:
@@ -25,6 +25,8 @@ module Sequel
25
25
  end
26
26
 
27
27
  module DatasetQuery
28
+ Dataset.def_mutation_method(:query, :module=>self)
29
+
28
30
  # Translates a query block into a dataset. Query blocks are an
29
31
  # alternative to Sequel's usual method chaining, by using
30
32
  # instance_eval with a proxy object:
@@ -24,7 +24,7 @@
24
24
  # You can load this extension into specific datasets:
25
25
  #
26
26
  # ds = DB[:table]
27
- # ds.extension(:query_literals)
27
+ # ds = ds.extension(:query_literals)
28
28
  #
29
29
  # Or you can load it into all of a database's datasets, which
30
30
  # is probably the desired behavior if you are using this extension:
@@ -5,7 +5,7 @@
5
5
  # You can load this extension into specific datasets:
6
6
  #
7
7
  # ds = DB[:table]
8
- # ds.extension(:select_remove)
8
+ # ds = ds.extension(:select_remove)
9
9
  #
10
10
  # Or you can load it into all of a database's datasets, which
11
11
  # is probably the desired behavior if you are using this extension:
@@ -12,7 +12,7 @@
12
12
  # You can load this extension into specific datasets:
13
13
  #
14
14
  # ds = DB[:table]
15
- # ds.extension(:sequel_3_dataset_methods)
15
+ # ds = ds.extension(:sequel_3_dataset_methods)
16
16
  #
17
17
  # Or you can load it into all of a database's datasets, which
18
18
  # is probably the desired behavior if you are using this extension:
@@ -7,7 +7,7 @@
7
7
  # You can load this extension into specific datasets:
8
8
  #
9
9
  # ds = DB[:table]
10
- # ds.extension(:set_overrides)
10
+ # ds = ds.extension(:set_overrides)
11
11
  #
12
12
  # Or you can load it into all of a database's datasets, which
13
13
  # is probably the desired behavior if you are using this extension:
data/lib/sequel/model.rb CHANGED
@@ -80,7 +80,7 @@ module Sequel
80
80
 
81
81
  # Class methods added to model that call the method of the same name on the dataset
82
82
  DATASET_METHODS = (Dataset::ACTION_METHODS + Dataset::QUERY_METHODS +
83
- [:each_server]) - [:and, :or, :[], :[]=, :columns, :columns!, :delete, :update, :add_graph_aliases]
83
+ [:each_server]) - [:and, :or, :[], :columns, :columns!, :delete, :update, :add_graph_aliases]
84
84
 
85
85
  # Boolean settings that can be modified at the global, class, or instance level.
86
86
  BOOLEAN_SETTINGS = [:typecast_empty_string_to_nil, :typecast_on_assignment, :strict_param_setting, \
@@ -1001,6 +1001,14 @@ module Sequel
1001
1001
  @changed_columns ||= []
1002
1002
  end
1003
1003
 
1004
+ # Similar to Model#dup, but copies frozen status to returned object
1005
+ # if current object is frozen.
1006
+ def clone
1007
+ o = dup
1008
+ o.freeze if frozen?
1009
+ o
1010
+ end
1011
+
1004
1012
  # Deletes and returns +self+. Does not run destroy hooks.
1005
1013
  # Look into using +destroy+ instead.
1006
1014
  #
@@ -1026,6 +1034,18 @@ module Sequel
1026
1034
  checked_save_failure(opts){checked_transaction(opts){_destroy(opts)}}
1027
1035
  end
1028
1036
 
1037
+ # Produce a shallow copy of the object, similar to Object#dup.
1038
+ def dup
1039
+ s = self
1040
+ super.instance_eval do
1041
+ @values = s.values.dup
1042
+ @changed_columns = s.changed_columns.dup
1043
+ @errors = s.errors.dup
1044
+ @this = s.this.dup if !new? && model.primary_key
1045
+ self
1046
+ end
1047
+ end
1048
+
1029
1049
  # Iterates through all of the current values using each.
1030
1050
  #
1031
1051
  # Album[1].each{|k, v| puts "#{k} => #{v}"}
@@ -1396,12 +1416,6 @@ module Sequel
1396
1416
  self
1397
1417
  end
1398
1418
 
1399
- # REMOVE41
1400
- def set_values(hash)
1401
- Sequel::Deprecation.deprecate('Model#set_values is deprecreated and will be removed in Sequel 4.1. Please use _refresh_set_values or _save_set_values or set the values directly.')
1402
- @values = hash
1403
- end
1404
-
1405
1419
  # Clear the setter_methods cache when a method is added
1406
1420
  def singleton_method_added(meth)
1407
1421
  @singleton_setter_added = true if meth.to_s =~ SETTER_METHOD_REGEXP
@@ -11,7 +11,7 @@ module Sequel
11
11
  end
12
12
  end
13
13
 
14
- # Deprecated alias for HookFailed, kept for backwards compatibility
14
+ # Alias for HookFailed, kept for backwards compatibility
15
15
  BeforeHookFailed = HookFailed
16
16
 
17
17
  # Exception class raised when +require_modification+ is set and an UPDATE or DELETE statement to modify the dataset doesn't
@@ -161,6 +161,15 @@ module Sequel
161
161
  @compositions ||= {}
162
162
  end
163
163
 
164
+ # Duplicate compositions hash when duplicating model instance.
165
+ def dup
166
+ s = self
167
+ super.instance_eval do
168
+ @compositions = s.compositions.dup
169
+ self
170
+ end
171
+ end
172
+
164
173
  # Freeze compositions hash when freezing model instance.
165
174
  def freeze
166
175
  compositions.freeze
@@ -85,6 +85,25 @@ module Sequel
85
85
  initial_values.has_key?(column)
86
86
  end
87
87
 
88
+ # Duplicate internal data structures
89
+ def dup
90
+ s = self
91
+ super.instance_eval do
92
+ @initial_values = s.initial_values.dup
93
+ @missing_initial_values = s.send(:missing_initial_values).dup
94
+ @previous_changes = s.previous_changes.dup if s.previous_changes
95
+ self
96
+ end
97
+ end
98
+
99
+ # Freeze internal data structures
100
+ def freeze
101
+ initial_values.freeze
102
+ missing_initial_values.freeze
103
+ @previous_changes.freeze if @previous_changes
104
+ super
105
+ end
106
+
88
107
  # The initial value of the given column. If the column value has
89
108
  # not changed, this will be the same as the current value of the
90
109
  # column.
@@ -101,14 +120,6 @@ module Sequel
101
120
  @initial_values ||= {}
102
121
  end
103
122
 
104
- # Freeze internal data structures
105
- def freeze
106
- initial_values.freeze
107
- missing_initial_values.freeze
108
- @previous_changes.freeze if @previous_changes
109
- super
110
- end
111
-
112
123
  # Reset the column to its initial value. If the column was not set
113
124
  # initial, removes it from the values.
114
125
  #
@@ -59,6 +59,15 @@ module Sequel
59
59
  clear_instance_filters
60
60
  end
61
61
 
62
+ # Duplicate internal structures when duplicating model instance.
63
+ def dup
64
+ ifs = instance_filters.dup
65
+ super.instance_eval do
66
+ @instance_filters = ifs
67
+ self
68
+ end
69
+ end
70
+
62
71
  # Freeze the instance filters when freezing the object
63
72
  def freeze
64
73
  instance_filters.freeze
@@ -172,6 +172,15 @@ module Sequel
172
172
  @deserialized_values ||= {}
173
173
  end
174
174
 
175
+ # Freeze the deserialized values
176
+ def dup
177
+ dv = deserialized_values.dup
178
+ super.instance_eval do
179
+ @deserialized_values = dv
180
+ self
181
+ end
182
+ end
183
+
175
184
  # Freeze the deserialized values
176
185
  def freeze
177
186
  deserialized_values.freeze
@@ -45,6 +45,15 @@ module Sequel
45
45
  cc
46
46
  end
47
47
 
48
+ # Duplicate the original deserialized values when duplicating instance.
49
+ def dup
50
+ o = @original_deserialized_values
51
+ super.instance_eval do
52
+ @original_deserialized_values = o.dup if o
53
+ self
54
+ end
55
+ end
56
+
48
57
  # Freeze the original deserialized values when freezing the instance.
49
58
  def freeze
50
59
  @original_deserialized_values ||= {}
@@ -3,26 +3,45 @@ module Sequel
3
3
  # The static_cache plugin is designed for models that are not modified at all
4
4
  # in production use cases, or at least where modifications to them would usually
5
5
  # coincide with an application restart. When loaded into a model class, it
6
- # retrieves all rows in the database and staticly caches a ruby array and hash
6
+ # retrieves all rows in the database and statically caches a ruby array and hash
7
7
  # keyed on primary key containing all of the model instances. All of these instances
8
- # are frozen so they won't be modified unexpectedly.
8
+ # are frozen so they won't be modified unexpectedly, and before hooks disallow
9
+ # saving or destroying instances.
10
+ #
11
+ # You can use the :frozen=>false option to have this plugin return unfrozen
12
+ # instances. This is slower as it requires creating new objects, but it allows
13
+ # you to make changes to the object and save them. If you set the option to false,
14
+ # you are responsible for updating the cache manually (the pg_static_cache_updater
15
+ # extension can handle this automatically).
9
16
  #
10
17
  # The caches this plugin creates are used for the following things:
11
18
  #
12
19
  # * Primary key lookups (e.g. Model[1])
13
- # * Model.all calls
14
- # * Model.each calls
15
- # * Model.map calls without an argument
16
- # * Model.to_hash calls without an argument
20
+ # * Model.all
21
+ # * Model.each
22
+ # * Model.count (without an argument or block)
23
+ # * Model.map
24
+ # * Model.to_hash
25
+ # * Model.to_hash_groups
17
26
  #
18
27
  # Usage:
19
28
  #
20
- # # Cache the AlbumType class staticly
29
+ # # Cache the AlbumType class statically, disallowing any changes.
21
30
  # AlbumType.plugin :static_cache
31
+ #
32
+ # # Cache the AlbumType class statically, but return unfrozen instances
33
+ # # that can be modified.
34
+ # AlbumType.plugin :static_cache, :frozen=>false
22
35
  module StaticCache
23
- # Populate the static caches when loading the plugin.
24
- def self.configure(model)
25
- model.send(:load_cache)
36
+ # Populate the static caches when loading the plugin. Options:
37
+ # :frozen :: Whether retrieved model objects are frozen. The default is true,
38
+ # for better performance as the shared frozen objects can be used
39
+ # directly. If set to false, new instances are created.
40
+ def self.configure(model, opts=OPTS)
41
+ model.instance_eval do
42
+ @static_cache_frozen = opts.fetch(:frozen, true)
43
+ load_cache
44
+ end
26
45
  end
27
46
 
28
47
  module ClassMethods
@@ -32,7 +51,11 @@ module Sequel
32
51
  # An array of all of the model's frozen instances, without issuing a database
33
52
  # query.
34
53
  def all
35
- @all.dup
54
+ if @static_cache_frozen
55
+ @all.dup
56
+ else
57
+ map{|o| o}
58
+ end
36
59
  end
37
60
 
38
61
  # Get the number of records in the cache, without issuing a database query.
@@ -47,13 +70,17 @@ module Sequel
47
70
  # Return the frozen object with the given pk, or nil if no such object exists
48
71
  # in the cache, without issuing a database query.
49
72
  def cache_get_pk(pk)
50
- cache[pk]
73
+ static_cache_object(cache[pk])
51
74
  end
52
75
 
53
76
  # Yield each of the model's frozen instances to the block, without issuing a database
54
77
  # query.
55
78
  def each(&block)
56
- @all.each(&block)
79
+ if @static_cache_frozen
80
+ @all.each(&block)
81
+ else
82
+ @all.each{|o| yield(static_cache_object(o))}
83
+ end
57
84
  end
58
85
 
59
86
  # Use the cache instead of a query to get the results.
@@ -65,36 +92,47 @@ module Sequel
65
92
  else
66
93
  @all.map{|r| r[column]}
67
94
  end
95
+ elsif @static_cache_frozen
96
+ @all.map(&block)
97
+ elsif block
98
+ @all.map{|o| yield(static_cache_object(o))}
68
99
  else
69
- @all.map(&(Proc.new if block_given?))
100
+ all.map
70
101
  end
71
102
  end
72
103
 
73
104
  Plugins.after_set_dataset(self, :load_cache)
105
+ Plugins.inherited_instance_variables(self, :@static_cache_frozen=>nil)
74
106
 
75
107
  # Use the cache instead of a query to get the results.
76
108
  def to_hash(key_column = nil, value_column = nil)
77
- return cache.dup if key_column.nil? && value_column.nil?
109
+ if key_column.nil? && value_column.nil?
110
+ if @static_cache_frozen
111
+ return cache.dup
112
+ else
113
+ key_column = primary_key
114
+ end
115
+ end
78
116
 
79
117
  h = {}
80
118
  if value_column
81
119
  if value_column.is_a?(Array)
82
120
  if key_column.is_a?(Array)
83
- each{|r| h[r.values.values_at(*key_column)] = r.values.values_at(*value_column)}
121
+ @all.each{|r| h[r.values.values_at(*key_column)] = r.values.values_at(*value_column)}
84
122
  else
85
- each{|r| h[r[key_column]] = r.values.values_at(*value_column)}
123
+ @all.each{|r| h[r[key_column]] = r.values.values_at(*value_column)}
86
124
  end
87
125
  else
88
126
  if key_column.is_a?(Array)
89
- each{|r| h[r.values.values_at(*key_column)] = r[value_column]}
127
+ @all.each{|r| h[r.values.values_at(*key_column)] = r[value_column]}
90
128
  else
91
- each{|r| h[r[key_column]] = r[value_column]}
129
+ @all.each{|r| h[r[key_column]] = r[value_column]}
92
130
  end
93
131
  end
94
132
  elsif key_column.is_a?(Array)
95
- each{|r| h[r.values.values_at(*key_column)] = r}
133
+ @all.each{|r| h[r.values.values_at(*key_column)] = static_cache_object(r)}
96
134
  else
97
- each{|r| h[r[key_column]] = r}
135
+ @all.each{|r| h[r[key_column]] = static_cache_object(r)}
98
136
  end
99
137
  h
100
138
  end
@@ -105,31 +143,36 @@ module Sequel
105
143
  if value_column
106
144
  if value_column.is_a?(Array)
107
145
  if key_column.is_a?(Array)
108
- each{|r| (h[r.values.values_at(*key_column)] ||= []) << r.values.values_at(*value_column)}
146
+ @all.each{|r| (h[r.values.values_at(*key_column)] ||= []) << r.values.values_at(*value_column)}
109
147
  else
110
- each{|r| (h[r[key_column]] ||= []) << r.values.values_at(*value_column)}
148
+ @all.each{|r| (h[r[key_column]] ||= []) << r.values.values_at(*value_column)}
111
149
  end
112
150
  else
113
151
  if key_column.is_a?(Array)
114
- each{|r| (h[r.values.values_at(*key_column)] ||= []) << r[value_column]}
152
+ @all.each{|r| (h[r.values.values_at(*key_column)] ||= []) << r[value_column]}
115
153
  else
116
- each{|r| (h[r[key_column]] ||= []) << r[value_column]}
154
+ @all.each{|r| (h[r[key_column]] ||= []) << r[value_column]}
117
155
  end
118
156
  end
119
157
  elsif key_column.is_a?(Array)
120
- each{|r| (h[r.values.values_at(*key_column)] ||= []) << r}
158
+ @all.each{|r| (h[r.values.values_at(*key_column)] ||= []) << static_cache_object(r)}
121
159
  else
122
- each{|r| (h[r[key_column]] ||= []) << r}
160
+ @all.each{|r| (h[r[key_column]] ||= []) << static_cache_object(r)}
123
161
  end
124
162
  h
125
163
  end
126
164
 
165
+ # Ask whether modifications to this class are allowed.
166
+ def static_cache_allow_modifications?
167
+ !@static_cache_frozen
168
+ end
169
+
127
170
  private
128
171
 
129
172
  # Return the frozen object with the given pk, or nil if no such object exists
130
173
  # in the cache, without issuing a database query.
131
174
  def primary_key_lookup(pk)
132
- cache[pk]
175
+ static_cache_object(cache[pk])
133
176
  end
134
177
 
135
178
  # Reload the cache for this model by retrieving all of the instances in the dataset
@@ -141,6 +184,31 @@ module Sequel
141
184
  @all = a.freeze
142
185
  @cache = h.freeze
143
186
  end
187
+
188
+ # If :frozen=>false is not used, just return the argument. Otherwise,
189
+ # create a new instance with the arguments values if the argument is
190
+ # not nil.
191
+ def static_cache_object(o)
192
+ if @static_cache_frozen
193
+ o
194
+ elsif o
195
+ call(o.values.dup)
196
+ end
197
+ end
198
+ end
199
+
200
+ module InstanceMethods
201
+ # Disallowing destroying the object unless the :frozen=>false option was used.
202
+ def before_destroy
203
+ return false unless model.static_cache_allow_modifications?
204
+ super
205
+ end
206
+
207
+ # Disallowing saving the object unless the :frozen=>false option was used.
208
+ def before_save
209
+ return false unless model.static_cache_allow_modifications?
210
+ super
211
+ end
144
212
  end
145
213
  end
146
214
  end