sequel 4.6.0 → 4.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +32 -0
  3. data/doc/association_basics.rdoc +18 -0
  4. data/doc/migration.rdoc +30 -0
  5. data/doc/release_notes/4.7.0.txt +103 -0
  6. data/doc/security.rdoc +5 -0
  7. data/doc/sql.rdoc +21 -12
  8. data/doc/validations.rdoc +10 -2
  9. data/doc/virtual_rows.rdoc +22 -29
  10. data/lib/sequel/adapters/jdbc.rb +4 -1
  11. data/lib/sequel/adapters/jdbc/h2.rb +5 -0
  12. data/lib/sequel/adapters/odbc.rb +0 -1
  13. data/lib/sequel/adapters/postgres.rb +8 -1
  14. data/lib/sequel/adapters/shared/db2.rb +5 -0
  15. data/lib/sequel/adapters/shared/mssql.rb +1 -1
  16. data/lib/sequel/adapters/shared/oracle.rb +5 -0
  17. data/lib/sequel/adapters/shared/postgres.rb +5 -0
  18. data/lib/sequel/adapters/shared/sqlanywhere.rb +1 -1
  19. data/lib/sequel/adapters/shared/sqlite.rb +6 -1
  20. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +1 -1
  21. data/lib/sequel/database/schema_methods.rb +1 -0
  22. data/lib/sequel/database/transactions.rb +11 -26
  23. data/lib/sequel/dataset/actions.rb +3 -3
  24. data/lib/sequel/dataset/features.rb +5 -0
  25. data/lib/sequel/dataset/sql.rb +16 -1
  26. data/lib/sequel/model/associations.rb +22 -7
  27. data/lib/sequel/model/base.rb +2 -2
  28. data/lib/sequel/plugins/auto_validations.rb +5 -1
  29. data/lib/sequel/plugins/instance_hooks.rb +21 -3
  30. data/lib/sequel/plugins/pg_array_associations.rb +21 -5
  31. data/lib/sequel/plugins/update_or_create.rb +60 -0
  32. data/lib/sequel/plugins/validation_helpers.rb +5 -2
  33. data/lib/sequel/sql.rb +55 -9
  34. data/lib/sequel/version.rb +1 -1
  35. data/spec/adapters/postgres_spec.rb +25 -4
  36. data/spec/core/database_spec.rb +1 -1
  37. data/spec/core/dataset_spec.rb +1 -1
  38. data/spec/core/expression_filters_spec.rb +53 -1
  39. data/spec/extensions/auto_validations_spec.rb +18 -0
  40. data/spec/extensions/instance_hooks_spec.rb +14 -0
  41. data/spec/extensions/pg_array_associations_spec.rb +40 -0
  42. data/spec/extensions/to_dot_spec.rb +1 -1
  43. data/spec/extensions/update_or_create_spec.rb +81 -0
  44. data/spec/extensions/validation_helpers_spec.rb +15 -11
  45. data/spec/integration/associations_test.rb +1 -1
  46. data/spec/integration/database_test.rb +8 -0
  47. data/spec/integration/dataset_test.rb +15 -10
  48. data/spec/integration/type_test.rb +4 -0
  49. data/spec/model/associations_spec.rb +20 -0
  50. data/spec/spec_config.rb +1 -1
  51. metadata +364 -360
@@ -382,6 +382,11 @@ module Sequel
382
382
  end
383
383
  end
384
384
 
385
+ # DB2 supports quoted function names.
386
+ def supports_quoted_function_names?
387
+ true
388
+ end
389
+
385
390
  def _truncate_sql(table)
386
391
  # "TRUNCATE #{table} IMMEDIATE" is only for newer version of db2, so we
387
392
  # use the following one
@@ -301,7 +301,7 @@ module Sequel
301
301
  end
302
302
 
303
303
  DATABASE_ERROR_REGEXPS = {
304
- /Violation of UNIQUE KEY constraint/ => UniqueConstraintViolation,
304
+ /Violation of UNIQUE KEY constraint|Violation of PRIMARY KEY constraint.+Cannot insert duplicate key/ => UniqueConstraintViolation,
305
305
  /conflicted with the (FOREIGN KEY.*|REFERENCE) constraint/ => ForeignKeyConstraintViolation,
306
306
  /conflicted with the CHECK constraint/ => CheckConstraintViolation,
307
307
  /column does not allow nulls/ => NotNullConstraintViolation,
@@ -485,6 +485,11 @@ module Sequel
485
485
  sql << FROM
486
486
  source_list_append(sql, @opts[:from] || DUAL)
487
487
  end
488
+
489
+ # Oracle supports quoted function names.
490
+ def supports_quoted_function_names?
491
+ true
492
+ end
488
493
  end
489
494
  end
490
495
  end
@@ -1485,6 +1485,11 @@ module Sequel
1485
1485
  db.server_version(@opts[:server])
1486
1486
  end
1487
1487
 
1488
+ # PostgreSQL supports quoted function names.
1489
+ def supports_quoted_function_names?
1490
+ true
1491
+ end
1492
+
1488
1493
  # Concatenate the expressions with a space in between
1489
1494
  def full_text_string_join(cols)
1490
1495
  cols = Array(cols).map{|x| SQL::Function.new(:COALESCE, x, EMPTY_STRING)}
@@ -121,7 +121,7 @@ module Sequel
121
121
  private
122
122
 
123
123
  DATABASE_ERROR_REGEXPS = {
124
- /would not be unique/ => Sequel::UniqueConstraintViolation,
124
+ /would not be unique|Primary key for table.+is not unique/ => Sequel::UniqueConstraintViolation,
125
125
  /Column .* in table .* cannot be NULL/ => Sequel::NotNullConstraintViolation,
126
126
  /Constraint .* violated: Invalid value in table .*/ => Sequel::CheckConstraintViolation,
127
127
  /No primary key value for foreign key .* in table .*/ => Sequel::ForeignKeyConstraintViolation,
@@ -332,7 +332,7 @@ module Sequel
332
332
  end
333
333
 
334
334
  DATABASE_ERROR_REGEXPS = {
335
- /is not unique\z/ => UniqueConstraintViolation,
335
+ /(is|are) not unique\z/ => UniqueConstraintViolation,
336
336
  /foreign key constraint failed\z/ => ForeignKeyConstraintViolation,
337
337
  /\A(SQLITE ERROR 19 \(CONSTRAINT\) : )?constraint failed\z/ => ConstraintViolation,
338
338
  /may not be NULL\z/ => NotNullConstraintViolation,
@@ -692,6 +692,11 @@ module Sequel
692
692
  super unless @opts[:lock] == :update
693
693
  end
694
694
 
695
+ # SQLite supports quoted function names.
696
+ def supports_quoted_function_names?
697
+ true
698
+ end
699
+
695
700
  # SQLite treats a DELETE with no WHERE clause as a TRUNCATE
696
701
  def _truncate_sql(table)
697
702
  "DELETE FROM #{table}"
@@ -26,7 +26,7 @@ module Sequel
26
26
  sql = @opts[:append_sql] || ''
27
27
  subselect_sql_append(sql, unlimited.
28
28
  unordered.
29
- select_append{ROW_NUMBER(:over, :order=>order){}.as(rn)}.
29
+ select_append{ROW_NUMBER{}.over(:order=>order).as(rn)}.
30
30
  from_self(:alias=>dsa1).
31
31
  select(*columns).
32
32
  limit(@opts[:limit]).
@@ -44,6 +44,7 @@ module Sequel
44
44
  #
45
45
  # Options:
46
46
  # :ignore_errors :: Ignore any DatabaseErrors that are raised
47
+ # :name :: Name to use for index instead of default
47
48
  #
48
49
  # See <tt>alter_table</tt>.
49
50
  def add_index(table, columns, options=OPTS)
@@ -216,32 +216,17 @@ module Sequel
216
216
  SQL_BEGIN
217
217
  end
218
218
 
219
- if (! defined?(RUBY_ENGINE) or RUBY_ENGINE == 'ruby' or RUBY_ENGINE == 'rbx') and RUBY_VERSION < '1.9'
220
- # :nocov:
221
- # Whether to commit the current transaction. On ruby 1.8 and rubinius,
222
- # Thread.current.status is checked because Thread#kill skips rescue
223
- # blocks (so exception would be nil), but the transaction should
224
- # still be rolled back.
225
- def commit_or_rollback_transaction(exception, conn, opts)
226
- if exception
227
- false
228
- else
229
- if Thread.current.status == 'aborting'
230
- rollback_transaction(conn, opts)
231
- false
232
- else
233
- commit_transaction(conn, opts)
234
- true
235
- end
236
- end
237
- end
238
- # :nocov:
239
- else
240
- # Whether to commit the current transaction. On ruby 1.9 and JRuby,
241
- # transactions will be committed if Thread#kill is used on an thread
242
- # that has a transaction open, and there isn't a work around.
243
- def commit_or_rollback_transaction(exception, conn, opts)
244
- if exception
219
+ # Whether to commit the current transaction. Thread.current.status is
220
+ # checked because Thread#kill skips rescue blocks (so exception would be
221
+ # nil), but the transaction should still be rolled back. On Ruby 1.9 (but
222
+ # not 1.8 or 2.0), the thread status will still be "run", so Thread#kill
223
+ # will erroneously commit the transaction, and there isn't a workaround.
224
+ def commit_or_rollback_transaction(exception, conn, opts)
225
+ if exception
226
+ false
227
+ else
228
+ if Thread.current.status == 'aborting'
229
+ rollback_transaction(conn, opts)
245
230
  false
246
231
  else
247
232
  commit_transaction(conn, opts)
@@ -102,14 +102,14 @@ module Sequel
102
102
  if no_arg
103
103
  if block
104
104
  arg = Sequel.virtual_row(&block)
105
- aggregate_dataset.get{count(arg).as(count)}
105
+ aggregate_dataset.get{count(arg).as(:count)}
106
106
  else
107
- aggregate_dataset.get{count(:*){}.as(count)}.to_i
107
+ aggregate_dataset.get{count{}.*.as(:count)}.to_i
108
108
  end
109
109
  elsif block
110
110
  raise Error, 'cannot provide both argument and block to Dataset#count'
111
111
  else
112
- aggregate_dataset.get{count(arg).as(count)}
112
+ aggregate_dataset.get{count(arg).as(:count)}
113
113
  end
114
114
  end
115
115
 
@@ -167,6 +167,11 @@ module Sequel
167
167
  true
168
168
  end
169
169
 
170
+ # Whether the database supports quoting function names, false by default.
171
+ def supports_quoted_function_names?
172
+ false
173
+ end
174
+
170
175
  # Whether the RETURNING clause is used for the given dataset.
171
176
  # +type+ can be :insert, :update, or :delete.
172
177
  def uses_returning?(type)
@@ -743,7 +743,22 @@ module Sequel
743
743
 
744
744
  # Backbone of function_sql_append and emulated_function_sql_append.
745
745
  def _function_sql_append(sql, name, args)
746
- sql << name.to_s
746
+ case name
747
+ when SQL::Identifier
748
+ if supports_quoted_function_names?
749
+ literal_append(sql, name)
750
+ else
751
+ sql << name.value.to_s
752
+ end
753
+ when SQL::QualifiedIdentifier
754
+ if supports_quoted_function_names?
755
+ literal_append(sql, name)
756
+ else
757
+ sql << split_qualifiers(name).join(DOT)
758
+ end
759
+ else
760
+ sql << name.to_s
761
+ end
747
762
  if args.empty?
748
763
  sql << FUNCTION_EMPTY
749
764
  else
@@ -152,6 +152,12 @@ module Sequel
152
152
  {filter_by_associations_conditions_key=>ds}
153
153
  end
154
154
 
155
+ # Whether to handle silent modification failure when adding/removing
156
+ # associated records, false by default.
157
+ def handle_silent_modification_failure?
158
+ false
159
+ end
160
+
155
161
  # The limit and offset for this association (returned as a two element array).
156
162
  def limit_and_offset
157
163
  if (v = self[:limit]).is_a?(Array)
@@ -479,6 +485,11 @@ module Sequel
479
485
  :"#{underscore(demodulize(self[:model].name))}_id"
480
486
  end
481
487
 
488
+ # Handle silent failure of add/remove methods if raise_on_save_failure is false.
489
+ def handle_silent_modification_failure?
490
+ self[:raise_on_save_failure] == false
491
+ end
492
+
482
493
  # The hash key to use for the eager loading predicate (left side of IN (1, 2, 3))
483
494
  def predicate_key
484
495
  cached_fetch(:predicate_key){qualify_assoc(self[:key])}
@@ -975,6 +986,8 @@ module Sequel
975
986
  # to the model method call, where :primary_key_column refers to the underlying column.
976
987
  # Should only be used if the model method differs from the primary key column, in
977
988
  # conjunction with defining a model alias method for the primary key column.
989
+ # :raise_on_save_failure :: Do not raise exceptions for hook or validation failures when saving associated
990
+ # objects in the add/remove methods (return nil instead) [one_to_many only].
978
991
  # === :many_to_many
979
992
  # :graph_join_table_block :: The block to pass to +join_table+ for
980
993
  # the join table when eagerly loading the association via +eager_graph+.
@@ -1116,7 +1129,7 @@ module Sequel
1116
1129
  def apply_window_function_eager_limit_strategy(ds, opts)
1117
1130
  rn = ds.row_number_column
1118
1131
  limit, offset = opts.limit_and_offset
1119
- ds = ds.unordered.select_append{row_number(:over, :partition=>opts.predicate_key, :order=>ds.opts[:order]){}.as(rn)}.from_self
1132
+ ds = ds.unordered.select_append{row_number{}.over(:partition=>opts.predicate_key, :order=>ds.opts[:order]).as(rn)}.from_self
1120
1133
  ds = if opts[:type] == :one_to_one
1121
1134
  ds.where(rn => offset ? offset+1 : 1)
1122
1135
  elsif offset
@@ -1426,7 +1439,7 @@ module Sequel
1426
1439
  cks.each{|k| ck_nil_hash[k] = nil}
1427
1440
 
1428
1441
  unless opts[:read_only]
1429
- validate = opts[:validate]
1442
+ save_opts = {:validate=>opts[:validate]}
1430
1443
 
1431
1444
  if one_to_one
1432
1445
  setter = opts[:setter] || proc do |o|
@@ -1437,21 +1450,23 @@ module Sequel
1437
1450
  end
1438
1451
  checked_transaction do
1439
1452
  up_ds.update(ck_nil_hash)
1440
- o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save") if o
1453
+ o.save(save_opts) || raise(Sequel::Error, "invalid associated object, cannot save") if o
1441
1454
  end
1442
1455
  end
1443
1456
  association_module_private_def(opts._setter_method, opts, &setter)
1444
1457
  association_module_def(opts.setter_method, opts){|o| set_one_to_one_associated_object(opts, o)}
1445
1458
  else
1459
+ save_opts[:raise_on_failure] = opts[:raise_on_save_failure] != false
1460
+
1446
1461
  adder = opts[:adder] || proc do |o|
1447
1462
  cks.zip(cpks).each{|k, pk| o.send(:"#{k}=", send(pk))}
1448
- o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save")
1463
+ o.save(save_opts)
1449
1464
  end
1450
1465
  association_module_private_def(opts._add_method, opts, &adder)
1451
1466
 
1452
1467
  remover = opts[:remover] || proc do |o|
1453
1468
  cks.each{|k| o.send(:"#{k}=", nil)}
1454
- o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save")
1469
+ o.save(save_opts)
1455
1470
  end
1456
1471
  association_module_private_def(opts._remove_method, opts, &remover)
1457
1472
 
@@ -1590,7 +1605,7 @@ module Sequel
1590
1605
  raise(Sequel::Error, "model object #{inspect} does not have a primary key") if opts.dataset_need_primary_key? && !pk
1591
1606
  ensure_associated_primary_key(opts, o, *args)
1592
1607
  return if run_association_callbacks(opts, :before_add, o) == false
1593
- send(opts._add_method, o, *args)
1608
+ return if !send(opts._add_method, o, *args) && opts.handle_silent_modification_failure?
1594
1609
  if array = associations[opts[:name]] and !array.include?(o)
1595
1610
  array.push(o)
1596
1611
  end
@@ -1699,7 +1714,7 @@ module Sequel
1699
1714
  raise(Sequel::Error, "model object #{inspect} does not have a primary key") if opts.dataset_need_primary_key? && !pk
1700
1715
  raise(Sequel::Error, "associated object #{o.inspect} does not have a primary key") if opts.need_associated_primary_key? && !o.pk
1701
1716
  return if run_association_callbacks(opts, :before_remove, o) == false
1702
- send(opts._remove_method, o, *args)
1717
+ return if !send(opts._remove_method, o, *args) && opts.handle_silent_modification_failure?
1703
1718
  associations[opts[:name]].delete_if{|x| o === x} if associations.include?(opts[:name])
1704
1719
  remove_reciprocal_object(opts, o)
1705
1720
  run_association_callbacks(opts, :after_remove, o)
@@ -41,8 +41,8 @@ module Sequel
41
41
  attr_reader :primary_key
42
42
 
43
43
  # Whether to raise an error instead of returning nil on a failure
44
- # to save/create/save_changes/etc due to a validation failure or
45
- # a before_* hook returning false.
44
+ # to save/create/save_changes/update/destroy due to a validation failure or
45
+ # a before_* hook returning false (default: true).
46
46
  attr_accessor :raise_on_save_failure
47
47
 
48
48
  # Whether to raise an error when unable to typecast data for a column
@@ -136,7 +136,11 @@ module Sequel
136
136
 
137
137
  validates_schema_types if model.auto_validate_types?
138
138
 
139
- model.auto_validate_unique_columns.each{|cols| validates_unique(cols)}
139
+ unique_opts = {}
140
+ if model.respond_to?(:sti_dataset)
141
+ unique_opts[:dataset] = model.sti_dataset
142
+ end
143
+ model.auto_validate_unique_columns.each{|cols| validates_unique(cols, unique_opts)}
140
144
  end
141
145
  end
142
146
  end
@@ -4,7 +4,7 @@ module Sequel
4
4
  # by passing a block to a _hook method (e.g. before_save_hook{do_something}).
5
5
  # The block is executed when the hook is called (e.g. before_save).
6
6
  #
7
- # All of the standard hooks are supported, except for after_initialize.
7
+ # All of the standard hooks are supported.
8
8
  # Instance level before hooks are executed in reverse order of addition before
9
9
  # calling super. Instance level after hooks are executed in order of addition
10
10
  # after calling super. If any of the instance level before hook blocks return
@@ -16,6 +16,8 @@ module Sequel
16
16
  # be run the first time you save the object (creating it), and the before_update
17
17
  # hook will be run the second time you save the object (updating it), and no
18
18
  # hooks will be run the third time you save the object.
19
+ #
20
+ # Validation hooks are not cleared until after a successful save.
19
21
  #
20
22
  # Usage:
21
23
  #
@@ -27,7 +29,7 @@ module Sequel
27
29
  module InstanceHooks
28
30
  module InstanceMethods
29
31
  BEFORE_HOOKS = Sequel::Model::BEFORE_HOOKS
30
- AFTER_HOOKS = Sequel::Model::AFTER_HOOKS - [:after_initialize]
32
+ AFTER_HOOKS = Sequel::Model::AFTER_HOOKS
31
33
  HOOKS = BEFORE_HOOKS + AFTER_HOOKS
32
34
  HOOKS.each{|h| class_eval(<<-END , __FILE__, __LINE__+1)}
33
35
  def #{h}_hook(&block)
@@ -38,7 +40,7 @@ module Sequel
38
40
  END
39
41
 
40
42
  BEFORE_HOOKS.each{|h| class_eval("def #{h}; run_before_instance_hooks(:#{h}) == false ? false : super end", __FILE__, __LINE__)}
41
- AFTER_HOOKS.each{|h| class_eval(<<-END, __FILE__, __LINE__ + 1)}
43
+ (AFTER_HOOKS - [:after_validation, :after_save]).each{|h| class_eval(<<-END, __FILE__, __LINE__ + 1)}
42
44
  def #{h}
43
45
  super
44
46
  run_after_instance_hooks(:#{h})
@@ -46,6 +48,22 @@ module Sequel
46
48
  @instance_hooks.delete(:#{h.to_s.sub('after', 'before')})
47
49
  end
48
50
  END
51
+
52
+ # Run after validation hooks, without clearing the validation hooks.
53
+ def after_validation
54
+ super
55
+ run_after_instance_hooks(:after_validation)
56
+ end
57
+
58
+ # Run after save hooks, clearing both the save and validation hooks.
59
+ def after_save
60
+ super
61
+ run_after_instance_hooks(:after_save)
62
+ @instance_hooks.delete(:after_save)
63
+ @instance_hooks.delete(:before_save)
64
+ @instance_hooks.delete(:after_validation)
65
+ @instance_hooks.delete(:before_validation)
66
+ end
49
67
 
50
68
  private
51
69
 
@@ -52,6 +52,8 @@ module Sequel
52
52
  # such as when this plugin needs to create an array type,
53
53
  # and typecasting is turned off or not setup correctly
54
54
  # for the model object.
55
+ # :raise_on_save_failure :: Do not raise exceptions for hook or validation failures when saving associated
56
+ # objects in the add/remove methods (return nil instead).
55
57
  # :save_after_modify :: For pg_array_to_many associations, this makes the
56
58
  # the modification methods save the current object,
57
59
  # so they operate more similarly to the one_to_many
@@ -93,6 +95,11 @@ module Sequel
93
95
  :"#{underscore(demodulize(self[:model].name))}_ids"
94
96
  end
95
97
 
98
+ # Handle silent failure of add/remove methods if raise_on_save_failure is false.
99
+ def handle_silent_modification_failure?
100
+ self[:raise_on_save_failure] == false
101
+ end
102
+
96
103
  # The hash key to use for the eager loading predicate (left side of IN (1, 2, 3))
97
104
  def predicate_key
98
105
  cached_fetch(:predicate_key){qualify_assoc(self[:key_column])}
@@ -159,6 +166,12 @@ module Sequel
159
166
  :"#{singularize(self[:name])}_ids"
160
167
  end
161
168
 
169
+ # Handle silent failure of add/remove methods if raise_on_save_failure is false
170
+ # and save_after_modify is true.
171
+ def handle_silent_modification_failure?
172
+ self[:raise_on_save_failure] == false && self[:save_after_modify]
173
+ end
174
+
162
175
  # A qualified version of the associated primary key.
163
176
  def predicate_key
164
177
  cached_fetch(:predicate_key){qualify_assoc(primary_key)}
@@ -286,7 +299,8 @@ module Sequel
286
299
  def_association_dataset_methods(opts)
287
300
 
288
301
  unless opts[:read_only]
289
- validate = opts[:validate]
302
+ save_opts = {:validate=>opts[:validate]}
303
+ save_opts[:raise_on_failure] = opts[:raise_on_save_failure] != false
290
304
 
291
305
  array_type = opts[:array_type] ||= :integer
292
306
  adder = opts[:adder] || proc do |o|
@@ -295,14 +309,14 @@ module Sequel
295
309
  else
296
310
  o.send("#{key}=", Sequel.pg_array([send(pk)], array_type))
297
311
  end
298
- o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save")
312
+ o.save(save_opts)
299
313
  end
300
314
  association_module_private_def(opts._add_method, opts, &adder)
301
315
 
302
316
  remover = opts[:remover] || proc do |o|
303
317
  if (array = o.send(key)) && !array.empty?
304
318
  array.delete(send(pk))
305
- o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save")
319
+ o.save(save_opts)
306
320
  end
307
321
  end
308
322
  association_module_private_def(opts._remove_method, opts, &remover)
@@ -388,11 +402,13 @@ module Sequel
388
402
  def_association_dataset_methods(opts)
389
403
 
390
404
  unless opts[:read_only]
391
- validate = opts[:validate]
405
+ save_opts = {:validate=>opts[:validate]}
406
+ save_opts[:raise_on_failure] = opts[:raise_on_save_failure] != false
392
407
  array_type = opts[:array_type] ||= :integer
408
+
393
409
  if opts[:save_after_modify]
394
410
  save_after_modify = proc do |obj|
395
- obj.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save")
411
+ obj.save(save_opts)
396
412
  end
397
413
  end
398
414