sunstone 5.0.0.beta3 → 5.0.0.1

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.tm_properties +1 -0
  4. data/.travis.yml +36 -0
  5. data/README.md +1 -2
  6. data/Rakefile.rb +1 -1
  7. data/ext/active_record/associations/collection_association.rb +48 -6
  8. data/ext/active_record/attribute_methods.rb +25 -21
  9. data/ext/active_record/callbacks.rb +17 -0
  10. data/ext/active_record/finder_methods.rb +44 -2
  11. data/ext/active_record/persistence.rb +127 -1
  12. data/ext/active_record/relation.rb +13 -5
  13. data/ext/active_record/relation/calculations.rb +25 -0
  14. data/ext/active_record/statement_cache.rb +3 -2
  15. data/ext/active_record/transactions.rb +60 -0
  16. data/ext/arel/attributes/empty_relation.rb +31 -0
  17. data/ext/arel/attributes/relation.rb +3 -2
  18. data/lib/active_record/connection_adapters/sunstone/database_statements.rb +13 -2
  19. data/lib/active_record/connection_adapters/sunstone/schema_dumper.rb +16 -0
  20. data/lib/active_record/connection_adapters/sunstone/schema_statements.rb +2 -2
  21. data/lib/active_record/connection_adapters/sunstone/type/uuid.rb +21 -0
  22. data/lib/active_record/connection_adapters/sunstone_adapter.rb +54 -30
  23. data/lib/arel/collectors/sunstone.rb +6 -4
  24. data/lib/arel/visitors/sunstone.rb +61 -39
  25. data/lib/sunstone.rb +18 -11
  26. data/lib/sunstone/connection.rb +62 -22
  27. data/lib/sunstone/exception.rb +3 -0
  28. data/lib/sunstone/gis.rb +1 -0
  29. data/lib/sunstone/version.rb +2 -2
  30. data/sunstone.gemspec +4 -5
  31. data/test/active_record/associations/has_and_belongs_to_many_test.rb +12 -0
  32. data/test/active_record/associations/has_many_test.rb +72 -0
  33. data/test/active_record/eager_loading_test.rb +15 -0
  34. data/test/active_record/persistance_test.rb +190 -0
  35. data/test/active_record/preload_test.rb +16 -0
  36. data/test/active_record/query_test.rb +91 -0
  37. data/test/models.rb +91 -0
  38. data/test/sunstone/connection/configuration_test.rb +44 -0
  39. data/test/sunstone/connection/cookie_store_test.rb +37 -0
  40. data/test/sunstone/connection/request_helper_test.rb +105 -0
  41. data/test/sunstone/connection/send_request_test.rb +164 -0
  42. data/test/sunstone/connection_test.rb +2 -298
  43. data/test/test_helper.rb +45 -2
  44. metadata +52 -47
  45. data/ext/active_record/associations/builder/has_and_belongs_to_many.rb +0 -48
  46. data/ext/active_record/calculations.rb +0 -32
  47. data/ext/active_record/query_methods.rb +0 -30
  48. data/ext/active_record/relation/predicate_builder.rb +0 -23
  49. data/test/models/ship.rb +0 -14
  50. data/test/query_test.rb +0 -134
  51. data/test/sunstone/parser_test.rb +0 -124
  52. data/test/sunstone_test.rb +0 -303
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c5c130a7d5b7307e472bd22688ee0b8621db478e
4
- data.tar.gz: a3fde9dce1b08795ea7ff9a1217921fb57c099ef
3
+ metadata.gz: a1fc21e0215013059317cc971ba06e8a221343e2
4
+ data.tar.gz: 9c9f8128129d8d9ff50e398f7982f9288573a41f
5
5
  SHA512:
6
- metadata.gz: 383524737f0c0fefbec2c99d62d35ce23c6e3060f772e11ebac65418493cf2ee892e34cd637a6f86bbf5439275103c986887f4dd05494cb223a8cc51a9b67349
7
- data.tar.gz: df9fe4e2e39011303804a12fe26b49b99c84c3651c017a202fbcec907b8439708f4ae04ca72400c47cc5031687de9c179495e23a5644a71ec6666ff69bcd1590
6
+ metadata.gz: ccb44a14900ab3434926246a341740026723695e868283a0d09f3b4d6e41700da0aaf2f77f61c4bc69ca6fe3cdeee5204924fc67695b5ddcc09c211ae4a466a4
7
+ data.tar.gz: 2fb90af0945f9c7ae8dbd1dad2dc7c0072efa24dcd2bd3cf1b76507fb6754e4a06f6093117d2e94c5900624f7083effdcaab8d02a00ff648c28e268fa2fc46b7
data/.gitignore CHANGED
@@ -8,6 +8,7 @@
8
8
  /test/tmp/
9
9
  /test/version_tmp/
10
10
  /tmp/
11
+ .DS_Store
11
12
 
12
13
  ## Documentation cache and generated files:
13
14
  /.yardoc/
@@ -0,0 +1 @@
1
+ exclude = '{$exclude,log,bin,tmp,.tm_properties,coverage}'
@@ -0,0 +1,36 @@
1
+ language: ruby
2
+ rvm: 2.3.1
3
+
4
+ cache:
5
+ bundler: true
6
+ directories:
7
+ - /home/travis/.rvm/gems
8
+
9
+ addons:
10
+ postgresql: "9.4"
11
+
12
+ before_install:
13
+ - unset BUNDLE_GEMFILE
14
+
15
+ install:
16
+ - git clone https://github.com/rails/rails.git ~/build/rails
17
+
18
+ before_script:
19
+ - export RAILS_VERSION=`cat sunstone.gemspec | grep activerecord | grep -ow "[0-9\.]\{1,\}"`
20
+ - pushd ~/build/rails
21
+ - git checkout v$RAILS_VERSION
22
+ - sed -i "/require 'support\/connection'/a \$LOAD_PATH.unshift\(File.expand_path\('~\/build\/malomalo\/sunstone\/lib'\)\)\nrequire 'sunstone'" ~/build/rails/activerecord/test/cases/helper.rb
23
+ - cat ~/build/rails/Gemfile
24
+ - "sed -i \"/group :db do/a gem 'sunstone', path: File.expand_path\\('~\\/build\\/malomalo\\/sunstone'\\)\" ~/build/rails/Gemfile"
25
+ - cat ~/build/rails/Gemfile
26
+ - bundle install --jobs=3 --retry=3
27
+ - createdb activerecord_unittest
28
+ - createdb activerecord_unittest2
29
+ - mysql -e "create database IF NOT EXISTS activerecord_unittest DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci;"
30
+ - mysql -e "create database IF NOT EXISTS activerecord_unittest2 DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci;"
31
+ - gem environment gempath
32
+ - popd
33
+ - bundle install --jobs=3 --retry=3
34
+ - gem environment gempath
35
+
36
+ script: bundle exec rake test && cd ~/build/rails/activerecord && bundle exec rake test --verbose
data/README.md CHANGED
@@ -1,5 +1,4 @@
1
- Sunstone
2
- ========
1
+ # Sunstone [![Travis CI](https://travis-ci.org/malomalo/sunstone.svg)](https://travis-ci.org/malomalo/sunstone)
3
2
 
4
3
  An [ActiveRecord](https://rubygems.org/gems/activerecord) adapter for quering
5
4
  APIs over Standard API (https://github.com/waratuman/standardapi).
@@ -12,7 +12,7 @@ task :c => :console
12
12
  Rake::TestTask.new do |t|
13
13
  t.libs << 'test'
14
14
  t.test_files = FileList['test/**/*_test.rb']
15
- #t.warning = true
15
+ t.warning = false
16
16
  #t.verbose = true
17
17
  end
18
18
 
@@ -6,12 +6,22 @@ module ActiveRecord
6
6
  other_array.each { |val| raise_on_type_mismatch!(val) }
7
7
  original_target = load_target.dup
8
8
 
9
- if owner.instance_variable_get(:@updateing)
9
+ if owner.new_record?
10
+ replace_records(other_array, original_target)
11
+ elsif owner.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter) && owner.instance_variable_defined?(:@updating) && owner.instance_variable_get(:@updating)
10
12
  replace_common_records_in_memory(other_array, original_target)
11
- concat(other_array - original_target)
13
+
14
+ # Remove from target
15
+ (original_target - other_array).each { |record| callback(:before_remove, record) }
16
+ (original_target - other_array).each { |record| target.delete(record) }
17
+ (original_target - other_array).each { |record| callback(:after_remove, record) }
18
+
19
+ # Add to target
20
+ (other_array - original_target).each do |record|
21
+ add_to_target(record)
22
+ end
23
+
12
24
  other_array
13
- elsif owner.new_record?
14
- replace_records(other_array, original_target)
15
25
  else
16
26
  replace_common_records_in_memory(other_array, original_target)
17
27
  if other_array != original_target
@@ -22,17 +32,47 @@ module ActiveRecord
22
32
  end
23
33
  end
24
34
 
35
+
36
+ end
37
+
38
+ class HasManyAssociation
39
+
40
+ def insert_record(record, validate = true, raise = false)
41
+ set_owner_attributes(record)
42
+ set_inverse_instance(record)
43
+
44
+ if record.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter) && (!owner.instance_variable_defined?(:@updating) && owner.instance_variable_get(:@updating))
45
+ true
46
+ elsif raise
47
+ record.save!(:validate => validate)
48
+ else
49
+ record.save(:validate => validate)
50
+ end
51
+ end
52
+
53
+ private
54
+ def save_through_record(record)
55
+ return if record.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
56
+ build_through_record(record).save!
57
+ ensure
58
+ @through_records.delete(record.object_id)
59
+ end
60
+
25
61
  end
62
+
26
63
  end
27
64
  end
28
65
 
29
66
  module ActiveRecord
30
67
  module Persistence
68
+
31
69
  # Updates the attributes of the model from the passed-in hash and saves the
32
70
  # record, all wrapped in a transaction. If the object is invalid, the saving
33
71
  # will fail and false will be returned.
34
72
  def update(attributes)
35
- @updateing = true
73
+ @updating = :updating
74
+ $updating_model = self
75
+
36
76
  # The following transaction covers any possible database side-effects of the
37
77
  # attributes assignment. For example, setting the IDs of a child collection.
38
78
  with_transaction_returning_status do
@@ -40,7 +80,9 @@ module ActiveRecord
40
80
  save
41
81
  end
42
82
  ensure
43
- @updateing = false
83
+ @updating = false
84
+ $updating_model = nil
44
85
  end
86
+
45
87
  end
46
88
  end
@@ -12,15 +12,19 @@ module ActiveRecord
12
12
  attribute_names.each do |name|
13
13
  attrs[arel_table[name]] = typecasted_attribute_value(name)
14
14
  end
15
-
15
+
16
16
  if self.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
17
17
  self.class.reflect_on_all_associations.each do |reflection|
18
18
  if reflection.belongs_to?
19
- add_attributes_for_belongs_to_association(reflection, attrs)
19
+ if association(reflection.name).loaded? && association(reflection.name).target == $updating_model
20
+ attrs.delete(arel_table[reflection.foreign_key])
21
+ else
22
+ add_attributes_for_belongs_to_association(reflection, attrs)
23
+ end
20
24
  elsif reflection.has_one?
21
25
  add_attributes_for_has_one_association(reflection, attrs)
22
26
  elsif reflection.collection?
23
- add_attributes_for_collection_association(reflection, attrs)
27
+ add_attributes_for_collection_association(reflection, attrs, arel_table)
24
28
  end
25
29
  end
26
30
  end
@@ -29,7 +33,7 @@ module ActiveRecord
29
33
  end
30
34
 
31
35
  def add_attributes_for_belongs_to_association(reflection, attrs)
32
- key = :"add_attributes_for_belongs_to_association#{reflection.name}"
36
+ key = :"add_attributes_for_belongs_to_association_#{reflection.name}"
33
37
  @_already_called ||= {}
34
38
  return if @_already_called[key]
35
39
  @_already_called[key]=true
@@ -47,11 +51,11 @@ module ActiveRecord
47
51
  if record.new_record? || (autosave && record.changed_for_autosave?)
48
52
  if record.new_record?
49
53
  record.send(:arel_attributes_with_values_for_create, record.attribute_names).each do |k, v|
50
- attrs[Arel::Attributes::Relation.new(k, reflection.name)] = v
54
+ attrs[Arel::Attributes::Relation.new(k, reflection.name, false, true)] = v
51
55
  end
52
56
  else
53
57
  record.send(:arel_attributes_with_values_for_update, record.attribute_names).each do |k, v|
54
- attrs[Arel::Attributes::Relation.new(k, reflection.name)] = v
58
+ attrs[Arel::Attributes::Relation.new(k, reflection.name, false, true)] = v
55
59
  end
56
60
  end
57
61
  end
@@ -84,11 +88,11 @@ module ActiveRecord
84
88
 
85
89
  if record.new_record?
86
90
  record.send(:arel_attributes_with_values_for_create, record.attribute_names).each do |k, v|
87
- attrs[Arel::Attributes::Relation.new(k, reflection.name)] = v
91
+ attrs[Arel::Attributes::Relation.new(k, reflection.name, false, true)] = v
88
92
  end
89
93
  else
90
94
  record.send(:arel_attributes_with_values_for_update, record.attribute_names).each do |k, v|
91
- attrs[Arel::Attributes::Relation.new(k, reflection.name)] = v
95
+ attrs[Arel::Attributes::Relation.new(k, reflection.name, false, true)] = v
92
96
  end
93
97
  end
94
98
  end
@@ -96,7 +100,7 @@ module ActiveRecord
96
100
  end
97
101
  end
98
102
 
99
- def add_attributes_for_collection_association(reflection, attrs)
103
+ def add_attributes_for_collection_association(reflection, attrs, arel_table=nil)
100
104
  key = :"add_attributes_for_collection_association#{reflection.name}"
101
105
  @_already_called ||= {}
102
106
  return if @_already_called[key]
@@ -108,22 +112,22 @@ module ActiveRecord
108
112
 
109
113
  if association = association_instance_get(reflection.name)
110
114
  autosave = reflection.options[:autosave]
111
- if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
112
115
 
113
- records.each_with_index do |record, idx|
114
- next if record.destroyed?
116
+ attrs[Arel::Attributes::EmptyRelation.new(arel_table, reflection.name, true, true)] = [] if association.target.empty?
117
+
118
+ association.target.each_with_index do |record, idx|
119
+ next if record.destroyed?
115
120
 
116
- if record.new_record?
117
- record.send(:arel_attributes_with_values_for_create, record.attribute_names).each do |k, v|
118
- attrs[Arel::Attributes::Relation.new(k, reflection.name, idx)] = v
119
- end
120
- else
121
- record.send(:arel_attributes_with_values_for_update, record.attribute_names).each do |k, v|
122
- attrs[Arel::Attributes::Relation.new(k, reflection.name, idx)] = v
123
- end
121
+ if record.new_record?
122
+ record.send(:arel_attributes_with_values_for_create, record.attribute_names).each do |k, v|
123
+ attrs[Arel::Attributes::Relation.new(k, reflection.name, idx, true)] = v
124
+ end
125
+ else
126
+ record.send(:arel_attributes_with_values_for_update, record.attribute_names).each do |k, v|
127
+ attrs[Arel::Attributes::Relation.new(k, reflection.name, idx, true)] = v
124
128
  end
125
-
126
129
  end
130
+
127
131
  end
128
132
 
129
133
  # reconstruct the scope now that we know the owner's id
@@ -0,0 +1,17 @@
1
+ module ActiveRecord
2
+ module Callbacks
3
+ private
4
+
5
+ def create_or_update(*) #:nodoc:
6
+ if self.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
7
+ @_already_called ||= {}
8
+ self.class.reflect_on_all_associations.each do |r|
9
+ @_already_called[:"autosave_associated_records_for_#{r.name}"] = true
10
+ end
11
+ end
12
+
13
+ _run_save_callbacks { super }
14
+ end
15
+
16
+ end
17
+ end
@@ -1,3 +1,41 @@
1
+ module Arel
2
+ module Visitors
3
+ class ToSql < Arel::Visitors::Reduce
4
+
5
+ def visit_Arel_Attributes_Relation o, collector
6
+ visit(o.relation, collector)
7
+ end
8
+
9
+ end
10
+ end
11
+ end
12
+
13
+ module ActiveRecord
14
+ class PredicateBuilder # :nodoc:
15
+
16
+ def expand_from_hash(attributes)
17
+ return ["1=0"] if attributes.empty?
18
+
19
+ attributes.flat_map do |key, value|
20
+ if value.is_a?(Hash)
21
+ ka = associated_predicate_builder(key).expand_from_hash(value)
22
+ if self.table.instance_variable_get(:@klass).connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
23
+ ka.each { |k|
24
+ if k.left.is_a?(Arel::Attributes::Attribute) || k.left.is_a?(Arel::Attributes::Relation)
25
+ k.left = Arel::Attributes::Relation.new(k.left, key)
26
+ end
27
+ }
28
+ end
29
+ ka
30
+ else
31
+ expand(key, value)
32
+ end
33
+ end
34
+ end
35
+
36
+ end
37
+ end
38
+
1
39
  module ActiveRecord
2
40
  module FinderMethods
3
41
 
@@ -20,7 +58,11 @@ module ActiveRecord
20
58
  []
21
59
  else
22
60
  arel = relation.arel
23
- rows = connection.select_all(arel, 'SQL', arel.bind_values + relation.bound_attributes)
61
+ rows = if connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
62
+ connection.select_all(arel, 'SQL', arel.bind_values + relation.bound_attributes)
63
+ else
64
+ connection.select_all(arel, 'SQL', relation.bound_attributes)
65
+ end
24
66
  if join_dependency
25
67
  join_dependency.instantiate(rows, aliases)
26
68
  else
@@ -37,7 +79,7 @@ module ActiveRecord
37
79
  }
38
80
  }
39
81
 
40
- model_cache = Hash.new { |h,klass| h[klass] = {} }
82
+ model_cache = Hash.new { |h,kklass| h[kklass] = {} }
41
83
  parents = model_cache[self.base_class]
42
84
 
43
85
  result_set.each { |row_hash|
@@ -4,10 +4,36 @@ module ActiveRecord
4
4
  private
5
5
 
6
6
  def create_or_update(*args)
7
+ @updating = new_record? ? :creating : :updating
8
+ $updating_model = self
9
+
7
10
  raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
8
11
  result = new_record? ? _create_record : _update_record(*args)
12
+
13
+ if self.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter) && result != 0
14
+ row_hash = result.rows.first
15
+
16
+ seen = Hash.new { |h, parent_klass|
17
+ h[parent_klass] = Hash.new { |i, parent_id|
18
+ i[parent_id] = Hash.new { |j, child_klass| j[child_klass] = {} }
19
+ }
20
+ }
21
+
22
+ model_cache = Hash.new { |h,klass| h[klass] = {} }
23
+ parents = model_cache[self.class.base_class]
24
+
25
+ self.assign_attributes(row_hash.select{|k,v| self.class.column_names.include?(k.to_s) })
26
+ row_hash.select{|k,v| !self.class.column_names.include?(k.to_s) }.each do |relation_name, value|
27
+ assc = association(relation_name.to_sym)
28
+ assc.reset if assc.reflection.collection?
29
+ end
30
+
31
+ construct(self, row_hash.select{|k,v| !self.class.column_names.include?(k.to_s) }, seen, model_cache)
32
+ end
33
+
9
34
  result != false
10
- rescue Sunstone::Exception::BadRequest => e
35
+ # TODO: perhaps this can go further down the stack?
36
+ rescue Sunstone::Exception::BadRequest, Sunstone::Exception::Forbidden => e
11
37
  JSON.parse(e.message)['errors'].each do |field, message|
12
38
  if message.is_a?(Array)
13
39
  message.each { |m| errors.add(field, m) }
@@ -16,6 +42,106 @@ module ActiveRecord
16
42
  end
17
43
  end
18
44
  raise ActiveRecord::RecordInvalid
45
+ ensure
46
+ @updating = false
47
+ $updating_model = nil
48
+ end
49
+
50
+ # Creates a record with values matching those of the instance attributes
51
+ # and returns its id.
52
+ def _create_record(attribute_names = self.attribute_names)
53
+ attributes_values = arel_attributes_with_values_for_create(attribute_names)
54
+
55
+ new_id = self.class.unscoped.insert attributes_values
56
+
57
+ @new_record = false
58
+
59
+ if self.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
60
+ new_id
61
+ else
62
+ self.id ||= new_id if self.class.primary_key
63
+ id
64
+ end
65
+ end
66
+
67
+ #!!!! TODO: I am duplicated from finder_methods.....
68
+ def construct(parent, relations, seen, model_cache)
69
+ relations.each do |key, attributes|
70
+ reflection = parent.class.reflect_on_association(key)
71
+ next unless reflection
72
+
73
+ if reflection.collection?
74
+ other = parent.association(reflection.name)
75
+ other.loaded!
76
+ else
77
+ if parent.association_cached?(reflection.name)
78
+ model = parent.association(reflection.name).target
79
+ construct(model, attributes.select{|k,v| !reflection.klass.column_names.include?(k.to_s) }, seen, model_cache)
80
+ end
81
+ end
82
+
83
+ if !reflection.collection?
84
+ construct_association(parent, reflection, attributes, seen, model_cache)
85
+ else
86
+ attributes.each do |row|
87
+ construct_association(parent, reflection, row, seen, model_cache)
88
+ end
89
+ end
90
+
91
+ end
92
+ end
93
+
94
+ #!!!! TODO: I am duplicated from finder_methods.....
95
+ def construct_association(parent, reflection, attributes, seen, model_cache)
96
+ return if attributes.nil?
97
+
98
+ klass = if reflection.polymorphic?
99
+ parent.send(reflection.foreign_type).constantize.base_class
100
+ else
101
+ reflection.klass
102
+ end
103
+ id = attributes[klass.primary_key]
104
+ model = seen[parent.class.base_class][parent.id][klass][id]
105
+
106
+ if model
107
+ construct(model, attributes.select{|k,v| !klass.column_names.include?(k.to_s) }, seen, model_cache)
108
+
109
+ other = parent.association(reflection.name)
110
+
111
+ if reflection.collection?
112
+ other.target.push(model)
113
+ else
114
+ other.target = model
115
+ end
116
+
117
+ other.set_inverse_instance(model)
118
+ else
119
+ model = construct_model(parent, reflection, id, attributes.select{|k,v| klass.column_names.include?(k.to_s) }, seen, model_cache)
120
+ seen[parent.class.base_class][parent.id][model.class.base_class][id] = model
121
+ construct(model, attributes.select{|k,v| !klass.column_names.include?(k.to_s) }, seen, model_cache)
122
+ end
123
+ end
124
+
125
+ #!!!! TODO: I am duplicated from finder_methods.....
126
+ def construct_model(record, reflection, id, attributes, seen, model_cache)
127
+ klass = if reflection.polymorphic?
128
+ record.send(reflection.foreign_type).constantize
129
+ else
130
+ reflection.klass
131
+ end
132
+
133
+ model = model_cache[klass][id] ||= klass.instantiate(attributes)
134
+ other = record.association(reflection.name)
135
+
136
+ if reflection.collection?
137
+ other.target.push(model)
138
+ else
139
+ other.target = model
140
+ end
141
+
142
+ other.set_inverse_instance(model)
143
+ model
19
144
  end
145
+
20
146
  end
21
147
  end