sequel 3.3.0 → 3.4.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.
- 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
|