sequel 4.1.1 → 4.2.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 (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