sequel 3.3.0 → 3.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +62 -0
- data/README.rdoc +4 -4
- data/doc/release_notes/3.3.0.txt +1 -1
- data/doc/release_notes/3.4.0.txt +325 -0
- data/doc/sharding.rdoc +3 -3
- data/lib/sequel/adapters/amalgalite.rb +1 -1
- data/lib/sequel/adapters/firebird.rb +4 -9
- data/lib/sequel/adapters/jdbc.rb +21 -7
- data/lib/sequel/adapters/mysql.rb +2 -1
- data/lib/sequel/adapters/odbc.rb +7 -21
- data/lib/sequel/adapters/oracle.rb +1 -1
- data/lib/sequel/adapters/postgres.rb +6 -1
- data/lib/sequel/adapters/shared/mssql.rb +11 -0
- data/lib/sequel/adapters/shared/mysql.rb +8 -12
- data/lib/sequel/adapters/shared/oracle.rb +13 -0
- data/lib/sequel/adapters/shared/postgres.rb +5 -10
- data/lib/sequel/adapters/shared/sqlite.rb +21 -1
- data/lib/sequel/adapters/sqlite.rb +2 -2
- data/lib/sequel/core.rb +147 -11
- data/lib/sequel/database.rb +21 -9
- data/lib/sequel/dataset.rb +31 -6
- data/lib/sequel/dataset/convenience.rb +1 -1
- data/lib/sequel/dataset/sql.rb +76 -18
- data/lib/sequel/extensions/inflector.rb +2 -51
- data/lib/sequel/model.rb +16 -10
- data/lib/sequel/model/associations.rb +4 -1
- data/lib/sequel/model/base.rb +13 -6
- data/lib/sequel/model/default_inflections.rb +46 -0
- data/lib/sequel/model/inflections.rb +1 -51
- data/lib/sequel/plugins/boolean_readers.rb +52 -0
- data/lib/sequel/plugins/instance_hooks.rb +57 -0
- data/lib/sequel/plugins/lazy_attributes.rb +13 -1
- data/lib/sequel/plugins/nested_attributes.rb +171 -0
- data/lib/sequel/plugins/serialization.rb +35 -16
- data/lib/sequel/plugins/timestamps.rb +87 -0
- data/lib/sequel/plugins/validation_helpers.rb +8 -1
- data/lib/sequel/sql.rb +33 -0
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/sqlite_spec.rb +11 -6
- data/spec/core/core_sql_spec.rb +29 -0
- data/spec/core/database_spec.rb +16 -7
- data/spec/core/dataset_spec.rb +264 -20
- data/spec/extensions/boolean_readers_spec.rb +86 -0
- data/spec/extensions/inflector_spec.rb +67 -4
- data/spec/extensions/instance_hooks_spec.rb +133 -0
- data/spec/extensions/lazy_attributes_spec.rb +45 -5
- data/spec/extensions/nested_attributes_spec.rb +272 -0
- data/spec/extensions/serialization_spec.rb +64 -1
- data/spec/extensions/timestamps_spec.rb +150 -0
- data/spec/extensions/validation_helpers_spec.rb +18 -0
- data/spec/integration/dataset_test.rb +79 -2
- data/spec/integration/schema_test.rb +17 -0
- data/spec/integration/timezone_test.rb +55 -0
- data/spec/model/associations_spec.rb +19 -7
- data/spec/model/model_spec.rb +29 -0
- data/spec/model/record_spec.rb +36 -0
- data/spec/spec_config.rb +1 -1
- metadata +14 -2
@@ -80,57 +80,8 @@ class String
|
|
80
80
|
(@uncountables << words).flatten!
|
81
81
|
end
|
82
82
|
|
83
|
-
|
84
|
-
|
85
|
-
plural(/s$/i, 's')
|
86
|
-
plural(/(ax|test)is$/i, '\1es')
|
87
|
-
plural(/(octop|vir)us$/i, '\1i')
|
88
|
-
plural(/(alias|status)$/i, '\1es')
|
89
|
-
plural(/(bu)s$/i, '\1ses')
|
90
|
-
plural(/(buffal|tomat)o$/i, '\1oes')
|
91
|
-
plural(/([ti])um$/i, '\1a')
|
92
|
-
plural(/sis$/i, 'ses')
|
93
|
-
plural(/(?:([^f])fe|([lr])f)$/i, '\1\2ves')
|
94
|
-
plural(/(hive)$/i, '\1s')
|
95
|
-
plural(/([^aeiouy]|qu)y$/i, '\1ies')
|
96
|
-
plural(/(x|ch|ss|sh)$/i, '\1es')
|
97
|
-
plural(/(matr|vert|ind)ix|ex$/i, '\1ices')
|
98
|
-
plural(/([m|l])ouse$/i, '\1ice')
|
99
|
-
plural(/^(ox)$/i, '\1en')
|
100
|
-
plural(/(quiz)$/i, '\1zes')
|
101
|
-
|
102
|
-
singular(/s$/i, '')
|
103
|
-
singular(/(n)ews$/i, '\1ews')
|
104
|
-
singular(/([ti])a$/i, '\1um')
|
105
|
-
singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i, '\1\2sis')
|
106
|
-
singular(/(^analy)ses$/i, '\1sis')
|
107
|
-
singular(/([^f])ves$/i, '\1fe')
|
108
|
-
singular(/(hive)s$/i, '\1')
|
109
|
-
singular(/(tive)s$/i, '\1')
|
110
|
-
singular(/([lr])ves$/i, '\1f')
|
111
|
-
singular(/([^aeiouy]|qu)ies$/i, '\1y')
|
112
|
-
singular(/(s)eries$/i, '\1eries')
|
113
|
-
singular(/(m)ovies$/i, '\1ovie')
|
114
|
-
singular(/(x|ch|ss|sh)es$/i, '\1')
|
115
|
-
singular(/([m|l])ice$/i, '\1ouse')
|
116
|
-
singular(/(bus)es$/i, '\1')
|
117
|
-
singular(/(o)es$/i, '\1')
|
118
|
-
singular(/(shoe)s$/i, '\1')
|
119
|
-
singular(/(cris|ax|test)es$/i, '\1is')
|
120
|
-
singular(/(octop|vir)i$/i, '\1us')
|
121
|
-
singular(/(alias|status)es$/i, '\1')
|
122
|
-
singular(/^(ox)en/i, '\1')
|
123
|
-
singular(/(vert|ind)ices$/i, '\1ex')
|
124
|
-
singular(/(matr)ices$/i, '\1ix')
|
125
|
-
singular(/(quiz)zes$/i, '\1')
|
126
|
-
|
127
|
-
irregular('person', 'people')
|
128
|
-
irregular('man', 'men')
|
129
|
-
irregular('child', 'children')
|
130
|
-
irregular('sex', 'sexes')
|
131
|
-
irregular('move', 'moves')
|
132
|
-
|
133
|
-
uncountable(%w(equipment information rice money species series fish sheep))
|
83
|
+
Sequel.require('default_inflections', 'model')
|
84
|
+
instance_eval(&Sequel::DEFAULT_INFLECTIONS_PROC)
|
134
85
|
end
|
135
86
|
|
136
87
|
# Yield the Inflections module if a block is given, and return
|
data/lib/sequel/model.rb
CHANGED
@@ -17,7 +17,13 @@ module Sequel
|
|
17
17
|
# table_name # => :something
|
18
18
|
# end
|
19
19
|
def self.Model(source)
|
20
|
-
Model::ANONYMOUS_MODEL_CLASSES[source] ||=
|
20
|
+
Model::ANONYMOUS_MODEL_CLASSES[source] ||= if source.is_a?(Database)
|
21
|
+
c = Class.new(Model)
|
22
|
+
c.db = source
|
23
|
+
c
|
24
|
+
else
|
25
|
+
Class.new(Model).set_dataset(source)
|
26
|
+
end
|
21
27
|
end
|
22
28
|
|
23
29
|
# Sequel::Model is an object relational mapper built on top of Sequel core. Each
|
@@ -38,15 +44,15 @@ module Sequel
|
|
38
44
|
ANONYMOUS_MODEL_CLASSES = {}
|
39
45
|
|
40
46
|
# Class methods added to model that call the method of the same name on the dataset
|
41
|
-
DATASET_METHODS = %w'<< all avg count delete distinct
|
42
|
-
each each_page empty? except exclude filter first from from_self
|
47
|
+
DATASET_METHODS = %w'<< add_graph_aliases all avg count delete distinct
|
48
|
+
each each_page eager eager_graph empty? except exclude filter first from from_self
|
43
49
|
full_outer_join get graph grep group group_and_count group_by having import
|
44
|
-
inner_join insert insert_multiple intersect interval join join_table
|
45
|
-
last left_outer_join limit map multi_insert naked order order_by
|
46
|
-
order_more paginate print qualify query range reverse_order right_outer_join
|
47
|
-
select select_all select_more server set set_graph_aliases
|
48
|
-
single_value to_csv to_hash
|
49
|
-
update where with with_sql'.map{|x| x.to_sym}
|
50
|
+
inner_join insert insert_multiple intersect interval invert join join_table
|
51
|
+
last left_outer_join limit map max min multi_insert naked order order_by
|
52
|
+
order_more paginate print qualify query range reverse reverse_order right_outer_join
|
53
|
+
select select_all select_more server set set_defaults set_graph_aliases set_overrides
|
54
|
+
single_value sum to_csv to_hash truncate unfiltered ungraphed ungrouped union unlimited unordered
|
55
|
+
update where with with_recursive with_sql'.map{|x| x.to_sym}
|
50
56
|
|
51
57
|
# Class instance variables to set to nil when a subclass is created, for -w compliance
|
52
58
|
EMPTY_INSTANCE_VARIABLES = [:@overridable_methods_module, :@db]
|
@@ -102,7 +108,7 @@ module Sequel
|
|
102
108
|
@use_transactions = true
|
103
109
|
end
|
104
110
|
|
105
|
-
require %w"inflections plugins base exceptions errors", "model"
|
111
|
+
require %w"default_inflections inflections plugins base exceptions errors", "model"
|
106
112
|
if !defined?(::SEQUEL_NO_ASSOCIATIONS) && !ENV.has_key?('SEQUEL_NO_ASSOCIATIONS')
|
107
113
|
require 'associations', 'model'
|
108
114
|
Model.plugin Model::Associations
|
@@ -882,7 +882,10 @@ module Sequel
|
|
882
882
|
# Add the given associated object to the given association
|
883
883
|
def add_associated_object(opts, o, *args)
|
884
884
|
raise(Sequel::Error, "model object #{model} does not have a primary key") unless pk
|
885
|
-
|
885
|
+
if opts.need_associated_primary_key?
|
886
|
+
o.save if o.new?
|
887
|
+
raise(Sequel::Error, "associated object #{o.model} does not have a primary key") unless o.pk
|
888
|
+
end
|
886
889
|
return if run_association_callbacks(opts, :before_add, o) == false
|
887
890
|
send(opts._add_method, o, *args)
|
888
891
|
associations[opts[:name]].push(o) if associations.include?(opts[:name])
|
data/lib/sequel/model/base.rb
CHANGED
@@ -184,10 +184,10 @@ module Sequel
|
|
184
184
|
unless ivs.include?("@dataset")
|
185
185
|
db
|
186
186
|
begin
|
187
|
-
if self == Model
|
187
|
+
if self == Model || !@dataset
|
188
188
|
subclass.set_dataset(subclass.implicit_table_name) unless subclass.name.empty?
|
189
|
-
elsif
|
190
|
-
subclass.set_dataset(
|
189
|
+
elsif @dataset
|
190
|
+
subclass.set_dataset(@dataset.clone, :inherited=>true)
|
191
191
|
end
|
192
192
|
rescue
|
193
193
|
nil
|
@@ -260,6 +260,7 @@ module Sequel
|
|
260
260
|
# If a dataset is used, the model's database is changed to the given
|
261
261
|
# dataset. If a symbol is used, a dataset is created from the current
|
262
262
|
# database with the table name given. Other arguments raise an Error.
|
263
|
+
# Returns self.
|
263
264
|
#
|
264
265
|
# This changes the row_proc of the given dataset to return
|
265
266
|
# model objects, extends the dataset with the dataset_method_modules,
|
@@ -652,6 +653,12 @@ module Sequel
|
|
652
653
|
def keys
|
653
654
|
@values.keys
|
654
655
|
end
|
656
|
+
|
657
|
+
# Whether this object has been modified since last saved, used by
|
658
|
+
# save_changes to determine whether changes should be saved
|
659
|
+
def modified?
|
660
|
+
!changed_columns.empty?
|
661
|
+
end
|
655
662
|
|
656
663
|
# Returns true if the current instance represents a new record.
|
657
664
|
def new?
|
@@ -705,11 +712,11 @@ module Sequel
|
|
705
712
|
use_transaction ? db.transaction(opts){_save(columns, opts)} : _save(columns, opts)
|
706
713
|
end
|
707
714
|
|
708
|
-
# Saves only changed columns
|
709
|
-
#
|
715
|
+
# Saves only changed columns if the object has been modified.
|
716
|
+
# If the object has not been modified, returns nil. If unable to
|
710
717
|
# save, returns false unless raise_on_save_failure is true.
|
711
718
|
def save_changes
|
712
|
-
save(:changed=>true) || false
|
719
|
+
save(:changed=>true) || false if modified?
|
713
720
|
end
|
714
721
|
|
715
722
|
# Updates the instance with the supplied values with support for virtual
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Sequel
|
2
|
+
# Proc that is instance evaled to create the default inflections for both the
|
3
|
+
# model inflector and the inflector extension.
|
4
|
+
DEFAULT_INFLECTIONS_PROC = lambda do
|
5
|
+
plural(/$/, 's')
|
6
|
+
plural(/s$/i, 's')
|
7
|
+
plural(/(alias|(?:stat|octop|vir|b)us)$/i, '\1es')
|
8
|
+
plural(/(buffal|tomat)o$/i, '\1oes')
|
9
|
+
plural(/([ti])um$/i, '\1a')
|
10
|
+
plural(/sis$/i, 'ses')
|
11
|
+
plural(/(?:([^f])fe|([lr])f)$/i, '\1\2ves')
|
12
|
+
plural(/(hive)$/i, '\1s')
|
13
|
+
plural(/([^aeiouy]|qu)y$/i, '\1ies')
|
14
|
+
plural(/(x|ch|ss|sh)$/i, '\1es')
|
15
|
+
plural(/(matr|vert|ind)ix|ex$/i, '\1ices')
|
16
|
+
plural(/([m|l])ouse$/i, '\1ice')
|
17
|
+
|
18
|
+
singular(/s$/i, '')
|
19
|
+
singular(/([ti])a$/i, '\1um')
|
20
|
+
singular(/(analy|ba|cri|diagno|parenthe|progno|synop|the)ses$/i, '\1sis')
|
21
|
+
singular(/([^f])ves$/i, '\1fe')
|
22
|
+
singular(/([h|t]ive)s$/i, '\1')
|
23
|
+
singular(/([lr])ves$/i, '\1f')
|
24
|
+
singular(/([^aeiouy]|qu)ies$/i, '\1y')
|
25
|
+
singular(/(m)ovies$/i, '\1ovie')
|
26
|
+
singular(/(x|ch|ss|sh)es$/i, '\1')
|
27
|
+
singular(/([m|l])ice$/i, '\1ouse')
|
28
|
+
singular(/buses$/i, 'bus')
|
29
|
+
singular(/oes$/i, 'o')
|
30
|
+
singular(/shoes$/i, 'shoe')
|
31
|
+
singular(/(alias|(?:stat|octop|vir|b)us)es$/i, '\1')
|
32
|
+
singular(/(vert|ind)ices$/i, '\1ex')
|
33
|
+
singular(/matrices$/i, 'matrix')
|
34
|
+
|
35
|
+
irregular('person', 'people')
|
36
|
+
irregular('man', 'men')
|
37
|
+
irregular('child', 'children')
|
38
|
+
irregular('sex', 'sexes')
|
39
|
+
irregular('move', 'moves')
|
40
|
+
irregular('ox', 'oxen')
|
41
|
+
irregular('quiz', 'quizzes')
|
42
|
+
irregular('testis', 'testes')
|
43
|
+
|
44
|
+
uncountable(%w(equipment information rice money species series fish sheep news))
|
45
|
+
end
|
46
|
+
end
|
@@ -96,57 +96,7 @@ module Sequel
|
|
96
96
|
(@uncountables << words).flatten!
|
97
97
|
end
|
98
98
|
|
99
|
-
|
100
|
-
plural(/$/, 's')
|
101
|
-
plural(/s$/i, 's')
|
102
|
-
plural(/(ax|test)is$/i, '\1es')
|
103
|
-
plural(/(octop|vir)us$/i, '\1i')
|
104
|
-
plural(/(alias|status)$/i, '\1es')
|
105
|
-
plural(/(bu)s$/i, '\1ses')
|
106
|
-
plural(/(buffal|tomat)o$/i, '\1oes')
|
107
|
-
plural(/([ti])um$/i, '\1a')
|
108
|
-
plural(/sis$/i, 'ses')
|
109
|
-
plural(/(?:([^f])fe|([lr])f)$/i, '\1\2ves')
|
110
|
-
plural(/(hive)$/i, '\1s')
|
111
|
-
plural(/([^aeiouy]|qu)y$/i, '\1ies')
|
112
|
-
plural(/(x|ch|ss|sh)$/i, '\1es')
|
113
|
-
plural(/(matr|vert|ind)ix|ex$/i, '\1ices')
|
114
|
-
plural(/([m|l])ouse$/i, '\1ice')
|
115
|
-
plural(/^(ox)$/i, '\1en')
|
116
|
-
plural(/(quiz)$/i, '\1zes')
|
117
|
-
|
118
|
-
singular(/s$/i, '')
|
119
|
-
singular(/(n)ews$/i, '\1ews')
|
120
|
-
singular(/([ti])a$/i, '\1um')
|
121
|
-
singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i, '\1\2sis')
|
122
|
-
singular(/(^analy)ses$/i, '\1sis')
|
123
|
-
singular(/([^f])ves$/i, '\1fe')
|
124
|
-
singular(/(hive)s$/i, '\1')
|
125
|
-
singular(/(tive)s$/i, '\1')
|
126
|
-
singular(/([lr])ves$/i, '\1f')
|
127
|
-
singular(/([^aeiouy]|qu)ies$/i, '\1y')
|
128
|
-
singular(/(s)eries$/i, '\1eries')
|
129
|
-
singular(/(m)ovies$/i, '\1ovie')
|
130
|
-
singular(/(x|ch|ss|sh)es$/i, '\1')
|
131
|
-
singular(/([m|l])ice$/i, '\1ouse')
|
132
|
-
singular(/(bus)es$/i, '\1')
|
133
|
-
singular(/(o)es$/i, '\1')
|
134
|
-
singular(/(shoe)s$/i, '\1')
|
135
|
-
singular(/(cris|ax|test)es$/i, '\1is')
|
136
|
-
singular(/(octop|vir)i$/i, '\1us')
|
137
|
-
singular(/(alias|status)es$/i, '\1')
|
138
|
-
singular(/^(ox)en/i, '\1')
|
139
|
-
singular(/(vert|ind)ices$/i, '\1ex')
|
140
|
-
singular(/(matr)ices$/i, '\1ix')
|
141
|
-
singular(/(quiz)zes$/i, '\1')
|
142
|
-
|
143
|
-
irregular('person', 'people')
|
144
|
-
irregular('man', 'men')
|
145
|
-
irregular('child', 'children')
|
146
|
-
irregular('sex', 'sexes')
|
147
|
-
irregular('move', 'moves')
|
148
|
-
|
149
|
-
uncountable(%w(equipment information rice money species series fish sheep))
|
99
|
+
instance_eval(&DEFAULT_INFLECTIONS_PROC)
|
150
100
|
|
151
101
|
private
|
152
102
|
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Plugins
|
3
|
+
# The BooleaReaders plugin allows for the creation of attribute? methods
|
4
|
+
# for boolean columns, which provide a nicer API. By default, the accessors
|
5
|
+
# are created for all columns of type :boolean. However, you can provide a
|
6
|
+
# block to the plugin to change the criteria used to determine if a
|
7
|
+
# column is boolean:
|
8
|
+
#
|
9
|
+
# Sequel::Model.plugin(:boolean_readers){|c| db_schema[c][:db_type] =~ /\Atinyint/}
|
10
|
+
#
|
11
|
+
# This may be useful if you are using MySQL and have some tinyint columns
|
12
|
+
# that represent booleans and others that represent integers. You can turn
|
13
|
+
# the convert_tinyint_to_bool setting off and use the attribute methods for
|
14
|
+
# the integer value and the attribute? methods for the boolean value.
|
15
|
+
module BooleanReaders
|
16
|
+
# Default proc for determining if given column is a boolean, which
|
17
|
+
# just checks that the :type is boolean.
|
18
|
+
DEFAULT_BOOLEAN_ATTRIBUTE_PROC = lambda{|c| db_schema[c][:type] == :boolean}
|
19
|
+
|
20
|
+
# Add the boolean_attribute? class method to the model, and create
|
21
|
+
# attribute? boolean reader methods for the class's columns if the class has a dataset.
|
22
|
+
def self.configure(model, &block)
|
23
|
+
model.meta_def(:boolean_attribute?, &(block || DEFAULT_BOOLEAN_ATTRIBUTE_PROC))
|
24
|
+
model.instance_eval{send(:create_boolean_readers) if @dataset}
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
# Create boolean readers for the class using the columns from the new dataset.
|
29
|
+
def set_dataset(*args)
|
30
|
+
super
|
31
|
+
create_boolean_readers
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Add a attribute? method for the column to a module included in the class.
|
38
|
+
def create_boolean_reader(column)
|
39
|
+
overridable_methods_module.module_eval do
|
40
|
+
define_method("#{column}?"){model.db.typecast_value(:boolean, send(column))}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Add attribute? methods for all of the boolean attributes for this model.
|
45
|
+
def create_boolean_readers
|
46
|
+
im = instance_methods.collect{|x| x.to_s}
|
47
|
+
columns.each{|c| create_boolean_reader(c) if boolean_attribute?(c) && !im.include?("#{c}?")}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Plugins
|
3
|
+
# The instance_hooks plugin allows you to add hooks to specific instances,
|
4
|
+
# by passing a block to a _hook method (e.g. before_save_hook{do_something}).
|
5
|
+
# The block executed when the hook is called (e.g. before_save).
|
6
|
+
#
|
7
|
+
# All of the standard hooks are supported, except for after_initialize.
|
8
|
+
# Instance level before hooks are executed in reverse order of addition before
|
9
|
+
# calling super. Instance level after hooks are executed in order of addition
|
10
|
+
# after calling super. If any of the instance level before hook blocks return
|
11
|
+
# false, no more instance level before hooks are called and false is returned.
|
12
|
+
#
|
13
|
+
# Instance level hooks are cleared when the object is saved successfully.
|
14
|
+
module InstanceHooks
|
15
|
+
module InstanceMethods
|
16
|
+
HOOKS = Sequel::Model::HOOKS - [:after_initialize]
|
17
|
+
HOOKS.each{|h| class_eval("def #{h}_hook(&block); add_instance_hook(:#{h}, &block) end", __FILE__, __LINE__)}
|
18
|
+
|
19
|
+
BEFORE_HOOKS, AFTER_HOOKS = HOOKS.partition{|hook| hook.to_s =~ /\Abefore_/}
|
20
|
+
BEFORE_HOOKS.each{|h| class_eval("def #{h}; run_instance_hooks(:#{h}) == false ? false : super end", __FILE__, __LINE__)}
|
21
|
+
AFTER_HOOKS.each{|h| class_eval("def #{h}; super; run_instance_hooks(:#{h}) end", __FILE__, __LINE__)}
|
22
|
+
|
23
|
+
# Clear the instance level hooks after saving the object.
|
24
|
+
def after_save
|
25
|
+
super
|
26
|
+
run_instance_hooks(:after_save)
|
27
|
+
@instance_hooks.clear if @instance_hooks
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Add the block as an instance level hook. For before hooks, add it to
|
33
|
+
# the beginning of the instance hook's array. For after hooks, add it
|
34
|
+
# to the end.
|
35
|
+
def add_instance_hook(hook, &block)
|
36
|
+
instance_hooks(hook).send(BEFORE_HOOKS.include?(hook) ? :unshift : :push, block)
|
37
|
+
end
|
38
|
+
|
39
|
+
# An array of instance level hook blocks for the given hook type.
|
40
|
+
def instance_hooks(hook)
|
41
|
+
@instance_hooks ||= {}
|
42
|
+
@instance_hooks[hook] ||= []
|
43
|
+
end
|
44
|
+
|
45
|
+
# Run all hook blocks of the given hook type. If a before hook,
|
46
|
+
# immediately return false if any hook block call returns false.
|
47
|
+
def run_instance_hooks(hook)
|
48
|
+
if BEFORE_HOOKS.include?(hook)
|
49
|
+
instance_hooks(hook).each{|b| return false if b.call == false}
|
50
|
+
else
|
51
|
+
instance_hooks(hook).each{|b| b.call}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -29,12 +29,24 @@ module Sequel
|
|
29
29
|
end
|
30
30
|
|
31
31
|
module ClassMethods
|
32
|
+
# Module to store the lazy attribute getter methods, so they can
|
33
|
+
# be overridden and call super to get the lazy attribute behavior
|
34
|
+
attr_accessor :lazy_attributes_module
|
35
|
+
|
32
36
|
# Remove the given attributes from the list of columns selected by default.
|
33
37
|
# For each attribute given, create an accessor method that allows a lazy
|
34
38
|
# lookup of the attribute. Each attribute should be given as a symbol.
|
35
39
|
def lazy_attributes(*attrs)
|
36
40
|
set_dataset(dataset.select(*(columns - attrs)))
|
37
|
-
attrs.each
|
41
|
+
attrs.each{|a| define_lazy_attribute_getter(a)}
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Add a lazy attribute getter method to the lazy_attributes_module
|
47
|
+
def define_lazy_attribute_getter(a)
|
48
|
+
include(self.lazy_attributes_module ||= Module.new) unless lazy_attributes_module
|
49
|
+
lazy_attributes_module.class_eval do
|
38
50
|
define_method(a) do
|
39
51
|
if !values.include?(a) && !new?
|
40
52
|
lazy_attribute_lookup(a)
|
@@ -0,0 +1,171 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Plugins
|
3
|
+
# The nested_attributes plugin allows you to update attributes for associated
|
4
|
+
# objects directly through the parent object, similar to ActiveRecord's
|
5
|
+
# Nested Attributes feature.
|
6
|
+
#
|
7
|
+
# Nested attributes are created using the nested_attributes method:
|
8
|
+
#
|
9
|
+
# Artist.one_to_many :albums
|
10
|
+
# Artist.nested_attributes :albums
|
11
|
+
# a = Artist.new(:name=>'YJM',
|
12
|
+
# :albums_attributes=>[{:name=>'RF'}, {:name=>'MO'}])
|
13
|
+
# # No database activity yet
|
14
|
+
#
|
15
|
+
# a.save # Saves artist and both albums
|
16
|
+
# a.albums.map{|x| x.name} # ['RF', 'MO']
|
17
|
+
module NestedAttributes
|
18
|
+
# Depend on the instance_hooks plugin.
|
19
|
+
def self.apply(model)
|
20
|
+
model.plugin(:instance_hooks)
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
# Module to store the nested_attributes setter methods, so they can
|
25
|
+
# call be overridden and call super to get the default behavior
|
26
|
+
attr_accessor :nested_attributes_module
|
27
|
+
|
28
|
+
# Allow nested attributes to be set for the given associations. Options:
|
29
|
+
# * :destroy - Allow destruction of nested records.
|
30
|
+
# * :limit - For *_to_many associations, a limit on the number of records
|
31
|
+
# that will be processed, to prevent denial of service attacks.
|
32
|
+
# * :remove - Allow disassociation of nested records (can remove the associated
|
33
|
+
# object from the parent object, but not destroy the associated object).
|
34
|
+
# * :strict - Set to false to not raise an error message if a primary key
|
35
|
+
# is provided in a record, but it doesn't match an existing associated
|
36
|
+
# object.
|
37
|
+
#
|
38
|
+
# If a block is provided, it is passed each nested attribute hash. If
|
39
|
+
# the hash should be ignored, the block should return anything except false or nil.
|
40
|
+
def nested_attributes(*associations, &block)
|
41
|
+
include(self.nested_attributes_module ||= Module.new) unless nested_attributes_module
|
42
|
+
opts = associations.last.is_a?(Hash) ? associations.pop : {}
|
43
|
+
reflections = associations.map{|a| association_reflection(a) || raise(Error, "no association named #{a} for #{self}")}
|
44
|
+
reflections.each do |r|
|
45
|
+
r[:nested_attributes] = opts
|
46
|
+
r[:nested_attributes][:reject_if] ||= block
|
47
|
+
def_nested_attribute_method(r)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Add a nested attribute setter method to a module included in the
|
54
|
+
# class.
|
55
|
+
def def_nested_attribute_method(reflection)
|
56
|
+
nested_attributes_module.class_eval do
|
57
|
+
if reflection.returns_array?
|
58
|
+
define_method("#{reflection[:name]}_attributes=") do |array|
|
59
|
+
nested_attributes_list_setter(reflection, array)
|
60
|
+
end
|
61
|
+
else
|
62
|
+
define_method("#{reflection[:name]}_attributes=") do |h|
|
63
|
+
nested_attributes_setter(reflection, h)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
module InstanceMethods
|
71
|
+
# Assume if an instance has nested attributes set on it, that it has
|
72
|
+
# been modified even if none of the instance's columns have been modified.
|
73
|
+
def modified?
|
74
|
+
super || @has_nested_attributes
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Create a new associated object with the given attributes, validate
|
80
|
+
# it when the parent is validated, and save it when the object is saved.
|
81
|
+
def nested_attributes_create(reflection, attributes)
|
82
|
+
obj = reflection.associated_class.new(attributes)
|
83
|
+
after_validation_hook{validate_associated_object(reflection, obj)}
|
84
|
+
if reflection.returns_array?
|
85
|
+
after_save_hook{send(reflection.add_method, obj)}
|
86
|
+
else
|
87
|
+
before_save_hook{send(reflection.setter_method, obj.save)}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Find an associated object with the matching pk. If a matching option
|
92
|
+
# is not found and the :strict option is not false, raise an Error.
|
93
|
+
def nested_attributes_find(reflection, pk)
|
94
|
+
pk = pk.to_s
|
95
|
+
unless obj = Array(associated_objects = send(reflection[:name])).find{|x| x.pk.to_s == pk}
|
96
|
+
raise(Error, 'no associated object with that primary key does not exist') unless reflection[:nested_attributes][:strict] == false
|
97
|
+
end
|
98
|
+
obj
|
99
|
+
end
|
100
|
+
|
101
|
+
# Take an array or hash of attribute hashes and set each one individually.
|
102
|
+
# If a hash is provided it, sort it by key and then use the values.
|
103
|
+
# If there is a limit on the nested attributes for this association,
|
104
|
+
# make sure the length of the attributes_list is not greater than the limit.
|
105
|
+
def nested_attributes_list_setter(reflection, attributes_list)
|
106
|
+
attributes_list = attributes_list.sort_by{|x| x.to_s}.map{|k,v| v} if attributes_list.is_a?(Hash)
|
107
|
+
if (limit = reflection[:nested_attributes][:limit]) && attributes_list.length > limit
|
108
|
+
raise(Error, "number of nested attributes (#{attributes_list.length}) exceeds the limit (#{limit})")
|
109
|
+
end
|
110
|
+
attributes_list.each{|a| nested_attributes_setter(reflection, a)}
|
111
|
+
end
|
112
|
+
|
113
|
+
# Remove the matching associated object from the current object.
|
114
|
+
# If the :destroy option is given, destroy the object after disassociating it.
|
115
|
+
def nested_attributes_remove(reflection, pk, opts={})
|
116
|
+
if obj = nested_attributes_find(reflection, pk)
|
117
|
+
before_save_hook do
|
118
|
+
if reflection.returns_array?
|
119
|
+
send(reflection.remove_method, obj)
|
120
|
+
else
|
121
|
+
send(reflection.setter_method, nil)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
after_save_hook{obj.destroy} if opts[:destroy]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Modify the associated object based on the contents of the attribtues hash:
|
129
|
+
# * If a block was given to nested_attributes, call it with the attributes and return immediately if the block returns true.
|
130
|
+
# * If no primary key exists in the attributes hash, create a new object.
|
131
|
+
# * If _delete is a key in the hash and the :destroy option is used, destroy the matching associated object.
|
132
|
+
# * If _remove is a key in the hash and the :remove option is used, disassociated the matching associated object.
|
133
|
+
# * Otherwise, update the matching associated object with the contents of the hash.
|
134
|
+
def nested_attributes_setter(reflection, attributes)
|
135
|
+
return if (b = reflection[:nested_attributes][:reject_if]) && b.call(attributes)
|
136
|
+
@has_nested_attributes = true
|
137
|
+
klass = reflection.associated_class
|
138
|
+
if pk = attributes.delete(klass.primary_key) || attributes.delete(klass.primary_key.to_s)
|
139
|
+
if klass.db.send(:typecast_value_boolean, attributes[:_delete] || attributes['_delete']) && reflection[:nested_attributes][:destroy]
|
140
|
+
nested_attributes_remove(reflection, pk, :destroy=>true)
|
141
|
+
elsif klass.db.send(:typecast_value_boolean, attributes[:_remove] || attributes['_remove']) && reflection[:nested_attributes][:remove]
|
142
|
+
nested_attributes_remove(reflection, pk)
|
143
|
+
else
|
144
|
+
nested_attributes_update(reflection, pk, attributes)
|
145
|
+
end
|
146
|
+
else
|
147
|
+
nested_attributes_create(reflection, attributes)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Update the matching associated object with the attributes,
|
152
|
+
# validating it when the parent object is validated and saving it
|
153
|
+
# when the parent is saved.
|
154
|
+
def nested_attributes_update(reflection, pk, attributes)
|
155
|
+
if obj = nested_attributes_find(reflection, pk)
|
156
|
+
obj.set(attributes)
|
157
|
+
after_validation_hook{validate_associated_object(reflection, obj)}
|
158
|
+
after_save_hook{obj.save}
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Validate the given associated object, adding any validation error messages from the
|
163
|
+
# given object to the parent object.
|
164
|
+
def validate_associated_object(reflection, obj)
|
165
|
+
association = reflection[:name]
|
166
|
+
obj.errors.full_messages.each{|m| errors.add(association, m)} unless obj.valid?
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|