torque-postgresql 2.4.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.rdoc +0 -17
  3. data/lib/torque/postgresql/adapter/database_statements.rb +32 -74
  4. data/lib/torque/postgresql/adapter/oid/enum_set.rb +1 -1
  5. data/lib/torque/postgresql/adapter/oid.rb +0 -3
  6. data/lib/torque/postgresql/adapter/quoting.rb +12 -20
  7. data/lib/torque/postgresql/adapter/schema_creation.rb +1 -2
  8. data/lib/torque/postgresql/adapter/schema_definitions.rb +0 -37
  9. data/lib/torque/postgresql/adapter/schema_dumper.rb +2 -60
  10. data/lib/torque/postgresql/adapter/schema_statements.rb +2 -74
  11. data/lib/torque/postgresql/adapter.rb +2 -11
  12. data/lib/torque/postgresql/associations/belongs_to_many_association.rb +7 -6
  13. data/lib/torque/postgresql/associations/{association.rb → foreign_association.rb} +1 -4
  14. data/lib/torque/postgresql/associations/preloader/association.rb +53 -26
  15. data/lib/torque/postgresql/associations/preloader/loader_query.rb +36 -0
  16. data/lib/torque/postgresql/associations/preloader.rb +1 -0
  17. data/lib/torque/postgresql/associations.rb +6 -1
  18. data/lib/torque/postgresql/attributes/builder/period.rb +6 -2
  19. data/lib/torque/postgresql/auxiliary_statement/settings.rb +22 -75
  20. data/lib/torque/postgresql/auxiliary_statement.rb +40 -39
  21. data/lib/torque/postgresql/base.rb +13 -33
  22. data/lib/torque/postgresql/config.rb +3 -30
  23. data/lib/torque/postgresql/inheritance.rb +1 -3
  24. data/lib/torque/postgresql/migration/command_recorder.rb +2 -12
  25. data/lib/torque/postgresql/railtie.rb +1 -5
  26. data/lib/torque/postgresql/reflection/abstract_reflection.rb +44 -20
  27. data/lib/torque/postgresql/reflection/belongs_to_many_reflection.rb +2 -2
  28. data/lib/torque/postgresql/relation/auxiliary_statement.rb +15 -28
  29. data/lib/torque/postgresql/relation.rb +10 -12
  30. data/lib/torque/postgresql/schema_cache.rb +2 -7
  31. data/lib/torque/postgresql/version.rb +1 -1
  32. data/lib/torque/postgresql.rb +1 -2
  33. data/lib/torque-postgresql.rb +0 -1
  34. data/spec/schema.rb +14 -30
  35. data/spec/spec_helper.rb +1 -2
  36. data/spec/tests/arel_spec.rb +2 -4
  37. data/spec/tests/auxiliary_statement_spec.rb +35 -374
  38. data/spec/tests/belongs_to_many_spec.rb +2 -99
  39. data/spec/tests/distinct_on_spec.rb +1 -1
  40. data/spec/tests/enum_set_spec.rb +10 -10
  41. data/spec/tests/enum_spec.rb +0 -90
  42. data/spec/tests/has_many_spec.rb +0 -46
  43. data/spec/tests/relation_spec.rb +1 -1
  44. data/spec/tests/table_inheritance_spec.rb +15 -11
  45. metadata +11 -37
  46. data/lib/torque/postgresql/auxiliary_statement/recursive.rb +0 -149
  47. data/lib/torque/postgresql/table_name.rb +0 -41
  48. data/lib/torque/range.rb +0 -22
  49. data/spec/models/category.rb +0 -2
  50. data/spec/models/internal/user.rb +0 -5
  51. data/spec/tests/range_spec.rb +0 -36
  52. data/spec/tests/schema_spec.rb +0 -134
@@ -5,26 +5,16 @@ module Torque
5
5
  module Migration
6
6
  module CommandRecorder
7
7
 
8
- # Records the rename operation for types
8
+ # Records the rename operation for types.
9
9
  def rename_type(*args, &block)
10
10
  record(:rename_type, args, &block)
11
11
  end
12
12
 
13
- # Inverts the type rename operation
13
+ # Inverts the type name.
14
14
  def invert_rename_type(args)
15
15
  [:rename_type, args.reverse]
16
16
  end
17
17
 
18
- # Records the creation of a schema
19
- def create_schema(*args, &block)
20
- record(:create_schema, args, &block)
21
- end
22
-
23
- # Inverts the creation of a schema
24
- def invert_create_schema(args)
25
- [:drop_schema, [args.first]]
26
- end
27
-
28
18
  # Records the creation of the enum to be reverted.
29
19
  def create_enum(*args, &block)
30
20
  record(:create_enum, args, &block)
@@ -30,15 +30,11 @@ module Torque
30
30
  Torque::PostgreSQL::Attributes::Enum.lookup(name).sample
31
31
  end
32
32
 
33
- # Define the exposed constant for both types of auxiliary statements
33
+ # Define the exposed constant for auxiliary statements
34
34
  if torque_config.auxiliary_statement.exposed_class.present?
35
35
  *ns, name = torque_config.auxiliary_statement.exposed_class.split('::')
36
36
  base = ns.present? ? Object.const_get(ns.join('::')) : Object
37
37
  base.const_set(name, Torque::PostgreSQL::AuxiliaryStatement)
38
-
39
- *ns, name = torque_config.auxiliary_statement.exposed_recursive_class.split('::')
40
- base = ns.present? ? Object.const_get(ns.join('::')) : Object
41
- base.const_set(name, Torque::PostgreSQL::AuxiliaryStatement::Recursive)
42
38
  end
43
39
  end
44
40
  end
@@ -6,6 +6,9 @@ module Torque
6
6
  module AbstractReflection
7
7
  AREL_ATTR = ::Arel::Attributes::Attribute
8
8
 
9
+ ARR_NO_CAST = 'bigint'
10
+ ARR_CAST = 'bigint[]'
11
+
9
12
  # Check if the foreign key actually exists
10
13
  def connected_through_array?
11
14
  false
@@ -37,36 +40,39 @@ module Torque
37
40
  result
38
41
  end
39
42
 
40
- # Build the id constraint checking if both types are perfect matching.
41
- # The klass attribute (left side) will always be a column attribute
43
+ # Build the id constraint checking if both types are perfect matching
42
44
  def build_id_constraint(klass_attr, source_attr)
43
45
  return klass_attr.eq(source_attr) unless connected_through_array?
44
46
 
45
47
  # Klass and key are associated with the reflection Class
46
- klass_type = klass.columns_hash[join_keys.key.to_s]
47
-
48
- # Apply an ANY operation which checks if the single value on the left
49
- # side exists in the array on the right side
50
- if source_attr.is_a?(AREL_ATTR)
51
- any_value = [klass_attr, source_attr]
52
- any_value.reverse! if klass_type.try(:array?)
53
- return any_value.shift.eq(::Arel::Nodes::NamedFunction.new('ANY', any_value))
54
- end
48
+ klass_type = klass.columns_hash[join_primary_key.to_s]
49
+ # active_record and foreign_key are associated with the source Class
50
+ source_type = active_record.columns_hash[join_foreign_key.to_s]
51
+
52
+ # If both are attributes but the left side is not an array, and the
53
+ # right side is, use the ANY operation
54
+ any_operation = arel_array_to_any(klass_attr, source_attr, klass_type, source_type)
55
+ return klass_attr.eq(any_operation) if any_operation
55
56
 
56
57
  # If the left side is not an array, just use the IN condition
57
58
  return klass_attr.in(source_attr) unless klass_type.try(:array)
58
59
 
59
- # Build the overlap condition (array && array) ensuring that the right
60
- # side has the same type as the left side
61
- source_attr = ::Arel::Nodes.build_quoted(Array.wrap(source_attr))
62
- klass_attr.overlaps(source_attr.cast(klass_type.sql_type_metadata.sql_type))
60
+ # Decide if should apply a cast to ensure same type comparision
61
+ should_cast = klass_type.type.eql?(:integer) && source_type.type.eql?(:integer)
62
+ should_cast &= !klass_type.sql_type.eql?(source_type.sql_type)
63
+ should_cast |= !(klass_attr.is_a?(AREL_ATTR) && source_attr.is_a?(AREL_ATTR))
64
+
65
+ # Apply necessary transformations to values
66
+ klass_attr = cast_constraint_to_array(klass_type, klass_attr, should_cast)
67
+ source_attr = cast_constraint_to_array(source_type, source_attr, should_cast)
68
+
69
+ # Return the overlap condition
70
+ klass_attr.overlaps(source_attr)
63
71
  end
64
72
 
65
- if PostgreSQL::AR610
66
- # TODO: Deprecate this method
67
- def join_keys
68
- OpenStruct.new(key: join_primary_key, foreign_key: join_foreign_key)
69
- end
73
+ # TODO: Deprecate this method
74
+ def join_keys
75
+ OpenStruct.new(key: join_primary_key, foreign_key: join_foreign_key)
70
76
  end
71
77
 
72
78
  private
@@ -77,6 +83,24 @@ module Torque
77
83
 
78
84
  build_id_constraint(klass_attr, source_attr)
79
85
  end
86
+
87
+ # Prepare a value for an array constraint overlap condition
88
+ def cast_constraint_to_array(type, value, should_cast)
89
+ base_ready = type.try(:array) && value.is_a?(AREL_ATTR)
90
+ return value if base_ready && (type.sql_type.eql?(ARR_NO_CAST) || !should_cast)
91
+
92
+ value = ::Arel::Nodes.build_quoted(Array.wrap(value)) unless base_ready
93
+ value = value.cast(ARR_CAST) if should_cast
94
+ value
95
+ end
96
+
97
+ # Check if it's possible to turn both attributes into an ANY condition
98
+ def arel_array_to_any(klass_attr, source_attr, klass_type, source_type)
99
+ return unless !klass_type.try(:array) && source_type.try(:array) &&
100
+ klass_attr.is_a?(AREL_ATTR) && source_attr.is_a?(AREL_ATTR)
101
+
102
+ ::Arel::Nodes::NamedFunction.new('ANY', [source_attr])
103
+ end
80
104
  end
81
105
 
82
106
  ::ActiveRecord::Reflection::AbstractReflection.prepend(AbstractReflection)
@@ -25,7 +25,7 @@ module Torque
25
25
  end
26
26
 
27
27
  def foreign_key
28
- @foreign_key ||= options[:foreign_key]&.to_s || derive_foreign_key.freeze
28
+ @foreign_key ||= options[:foreign_key] || derive_foreign_key.freeze
29
29
  end
30
30
 
31
31
  def association_foreign_key
@@ -33,7 +33,7 @@ module Torque
33
33
  end
34
34
 
35
35
  def active_record_primary_key
36
- @active_record_primary_key ||= options[:primary_key]&.to_s || derive_primary_key
36
+ @active_record_primary_key ||= options[:primary_key] || derive_primary_key
37
37
  end
38
38
 
39
39
  def join_primary_key(*)
@@ -10,14 +10,22 @@ module Torque
10
10
  # :nodoc:
11
11
  def auxiliary_statements_values=(value); set_value(:auxiliary_statements, value); end
12
12
 
13
- # Set use of an auxiliary statement
14
- def with(*args, **settings)
15
- spawn.with!(*args, **settings)
13
+ # Set use of an auxiliary statement already configurated on the model
14
+ def with(*args)
15
+ spawn.with!(*args)
16
16
  end
17
17
 
18
18
  # Like #with, but modifies relation in place.
19
- def with!(*args, **settings)
20
- instantiate_auxiliary_statements(*args, **settings)
19
+ def with!(*args)
20
+ options = args.extract_options!
21
+ args.each do |table|
22
+ instance = table.is_a?(Class) && table < PostgreSQL::AuxiliaryStatement \
23
+ ? table.new(options) \
24
+ : PostgreSQL::AuxiliaryStatement.instantiate(table, self, options)
25
+
26
+ self.auxiliary_statements_values += [instance]
27
+ end
28
+
21
29
  self
22
30
  end
23
31
 
@@ -39,23 +47,8 @@ module Torque
39
47
  # Hook arel build to add the distinct on clause
40
48
  def build_arel(*)
41
49
  arel = super
42
- type = auxiliary_statement_type
43
- sub_queries = build_auxiliary_statements(arel)
44
- sub_queries.nil? ? arel : arel.with(*type, *sub_queries)
45
- end
46
-
47
- # Instantiate one or more auxiliary statements for the given +klass+
48
- def instantiate_auxiliary_statements(*args, **options)
49
- klass = PostgreSQL::AuxiliaryStatement
50
- klass = klass::Recursive if options.delete(:recursive).present?
51
-
52
- self.auxiliary_statements_values += args.map do |table|
53
- if table.is_a?(Class) && table < klass
54
- table.new(**options)
55
- else
56
- klass.instantiate(table, self, **options)
57
- end
58
- end
50
+ subqueries = build_auxiliary_statements(arel)
51
+ subqueries.nil? ? arel : arel.with(subqueries)
59
52
  end
60
53
 
61
54
  # Build all necessary data for auxiliary statements
@@ -66,12 +59,6 @@ module Torque
66
59
  end
67
60
  end
68
61
 
69
- # Return recursive if any auxiliary statement is recursive
70
- def auxiliary_statement_type
71
- klass = PostgreSQL::AuxiliaryStatement::Recursive
72
- :recursive if auxiliary_statements_values.any?(klass)
73
- end
74
-
75
62
  # Throw an error showing that an auxiliary statement of the given
76
63
  # table name isn't defined
77
64
  def auxiliary_statement_error(name)
@@ -19,6 +19,8 @@ module Torque
19
19
  MULTI_VALUE_METHODS = [:distinct_on, :auxiliary_statements, :cast_records, :select_extra]
20
20
  VALUE_METHODS = SINGLE_VALUE_METHODS + MULTI_VALUE_METHODS
21
21
 
22
+ ARColumn = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Column
23
+
22
24
  # :nodoc:
23
25
  def select_extra_values; get_value(:select_extra); end
24
26
  # :nodoc:
@@ -74,6 +76,14 @@ module Torque
74
76
  end
75
77
  end
76
78
 
79
+ # Serialize the given value so it can be used in a condition tha involves
80
+ # the given column
81
+ def cast_for_condition(column, value)
82
+ column = columns_hash[column.to_s] unless column.is_a?(ARColumn)
83
+ caster = connection.lookup_cast_type_from_column(column)
84
+ connection.type_cast(caster.serialize(value))
85
+ end
86
+
77
87
  private
78
88
 
79
89
  def build_arel(*)
@@ -140,18 +150,6 @@ module Torque
140
150
  ActiveRecord::QueryMethods::VALID_UNSCOPING_VALUES += %i[cast_records itself_only
141
151
  distinct_on auxiliary_statements]
142
152
 
143
- unless AR610
144
- Relation::SINGLE_VALUE_METHODS.each do |value|
145
- ActiveRecord::QueryMethods::DEFAULT_VALUES[value] = nil \
146
- if ActiveRecord::QueryMethods::DEFAULT_VALUES[value].nil?
147
- end
148
-
149
- Relation::MULTI_VALUE_METHODS.each do |value|
150
- ActiveRecord::QueryMethods::DEFAULT_VALUES[value] ||= \
151
- ActiveRecord::QueryMethods::FROZEN_EMPTY_ARRAY
152
- end
153
- end
154
-
155
153
  $VERBOSE = warn_level
156
154
  end
157
155
  end
@@ -109,6 +109,7 @@ module Torque
109
109
 
110
110
  # Try to find a model based on a given table
111
111
  def lookup_model(table_name, scoped_class = '')
112
+ # byebug if table_name == 'activities'
112
113
  scoped_class = scoped_class.name if scoped_class.is_a?(Class)
113
114
  return @data_sources_model_names[table_name] \
114
115
  if @data_sources_model_names.key?(table_name)
@@ -117,19 +118,13 @@ module Torque
117
118
  scopes = scoped_class.scan(/(?:::)?[A-Z][a-z]+/)
118
119
  scopes.unshift('Object::')
119
120
 
120
- # Check if the table name comes with a schema
121
- if table_name.include?('.')
122
- schema, table_name = table_name.split('.')
123
- scopes.insert(1, schema.camelize) if schema != 'public'
124
- end
125
-
126
121
  # Consider the maximum namespaced possible model name
127
122
  max_name = table_name.tr('_', '/').camelize.split(/(::)/)
128
123
  max_name[-1] = max_name[-1].singularize
129
124
 
130
125
  # Test all the possible names against all the possible scopes
131
126
  until scopes.size == 0
132
- scope = scopes.join.safe_constantize
127
+ scope = scopes.join.chomp('::').safe_constantize
133
128
  model = find_model(max_name, table_name, scope) unless scope.nil?
134
129
  return @data_sources_model_names[table_name] = model unless model.nil?
135
130
  scopes.pop
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Torque
4
4
  module PostgreSQL
5
- VERSION = '2.4.4'
5
+ VERSION = '3.0.0'
6
6
  end
7
7
  end
@@ -20,13 +20,12 @@ require 'torque/postgresql/associations'
20
20
  require 'torque/postgresql/attributes'
21
21
  require 'torque/postgresql/autosave_association'
22
22
  require 'torque/postgresql/auxiliary_statement'
23
+ require 'torque/postgresql/base'
23
24
  require 'torque/postgresql/inheritance'
24
- require 'torque/postgresql/base'# Needs to be after inheritance
25
25
  require 'torque/postgresql/insert_all'
26
26
  require 'torque/postgresql/migration'
27
27
  require 'torque/postgresql/relation'
28
28
  require 'torque/postgresql/reflection'
29
29
  require 'torque/postgresql/schema_cache'
30
- require 'torque/postgresql/table_name'
31
30
 
32
31
  require 'torque/postgresql/railtie' if defined?(Rails)
@@ -1,2 +1 @@
1
1
  require 'torque/postgresql'
2
- require 'torque/range'
data/spec/schema.rb CHANGED
@@ -10,7 +10,7 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- version = 3
13
+ version = 2
14
14
 
15
15
  return if ActiveRecord::Migrator.current_version == version
16
16
  ActiveRecord::Schema.define(version: version) do
@@ -20,16 +20,13 @@ ActiveRecord::Schema.define(version: version) do
20
20
  enable_extension "pgcrypto"
21
21
  enable_extension "plpgsql"
22
22
 
23
- # Custom schemas used in this database.
24
- create_schema "internal", force: :cascade
25
-
26
23
  # Custom types defined in this database.
27
24
  # Note that some types may not work with other database engines. Be careful if changing database.
28
- create_enum "content_status", ["created", "draft", "published", "archived"], force: :cascade
29
- create_enum "specialties", ["books", "movies", "plays"], force: :cascade
30
- create_enum "roles", ["visitor", "assistant", "manager", "admin"], force: :cascade
31
- create_enum "conflicts", ["valid", "invalid", "untrusted"], force: :cascade
32
- create_enum "types", ["A", "B", "C", "D"], force: :cascade
25
+ create_enum "content_status", ["created", "draft", "published", "archived"]
26
+ create_enum "specialties", ["books", "movies", "plays"]
27
+ create_enum "roles", ["visitor", "assistant", "manager", "admin"]
28
+ create_enum "conflicts", ["valid", "invalid", "untrusted"]
29
+ create_enum "types", ["A", "B", "C", "D"]
33
30
 
34
31
  create_table "geometries", force: :cascade do |t|
35
32
  t.point "point"
@@ -69,30 +66,24 @@ ActiveRecord::Schema.define(version: version) do
69
66
  t.enum "specialty", enum_type: :specialties
70
67
  end
71
68
 
72
- create_table "categories", force: :cascade do |t|
73
- t.integer "parent_id"
74
- t.string "title"
75
- end
76
-
77
69
  create_table "texts", force: :cascade do |t|
78
70
  t.integer "user_id"
79
71
  t.string "content"
80
- t.enum "conflict", enum_type: :conflicts
72
+ t.enum "conflict", enum_type: :conflicts
81
73
  end
82
74
 
83
75
  create_table "comments", force: :cascade do |t|
84
- t.integer "user_id", null: false
76
+ t.integer "user_id", null: false
85
77
  t.integer "comment_id"
86
78
  t.integer "video_id"
87
- t.text "content", null: false
79
+ t.text "content", null: false
88
80
  t.string "kind"
89
81
  t.index ["user_id"], name: "index_comments_on_user_id", using: :btree
90
82
  t.index ["comment_id"], name: "index_comments_on_comment_id", using: :btree
91
83
  end
92
84
 
93
85
  create_table "courses", force: :cascade do |t|
94
- t.integer "category_id"
95
- t.string "title", null: false
86
+ t.string "title", null: false
96
87
  t.interval "duration"
97
88
  t.enum "types", enum_type: :types, array: true
98
89
  t.datetime "created_at", null: false
@@ -108,7 +99,7 @@ ActiveRecord::Schema.define(version: version) do
108
99
  t.integer "activity_id"
109
100
  t.string "title"
110
101
  t.text "content"
111
- t.enum "status", enum_type: :content_status
102
+ t.enum "status", enum_type: :content_status
112
103
  t.index ["author_id"], name: "index_posts_on_author_id", using: :btree
113
104
  end
114
105
 
@@ -120,24 +111,17 @@ ActiveRecord::Schema.define(version: version) do
120
111
  end
121
112
 
122
113
  create_table "users", force: :cascade do |t|
123
- t.string "name", null: false
124
- t.enum "role", enum_type: :roles, default: :visitor
125
- t.datetime "created_at", null: false
126
- t.datetime "updated_at", null: false
127
- end
128
-
129
- create_table "users", schema: "internal", force: :cascade do |t|
130
- t.string "email"
114
+ t.string "name", null: false
115
+ t.enum "role", enum_type: :roles, default: :visitor
131
116
  t.datetime "created_at", null: false
132
117
  t.datetime "updated_at", null: false
133
- t.index ["email"], name: "index_internal_users_on_email", unique: true
134
118
  end
135
119
 
136
120
  create_table "activities", force: :cascade do |t|
137
121
  t.integer "author_id"
138
122
  t.string "title"
139
123
  t.boolean "active"
140
- t.enum "kind", enum_type: :types
124
+ t.enum "kind", enum_type: :types
141
125
  t.datetime "created_at", null: false
142
126
  t.datetime "updated_at", null: false
143
127
  end
data/spec/spec_helper.rb CHANGED
@@ -22,7 +22,7 @@ cleaner = ->() do
22
22
  end
23
23
 
24
24
  load File.join('schema.rb')
25
- Dir.glob(File.join('spec', '{models,factories,mocks}', '**', '*.rb')) do |file|
25
+ Dir.glob(File.join('spec', '{models,factories,mocks}', '*.rb')) do |file|
26
26
  require file[5..-4]
27
27
  end
28
28
 
@@ -39,7 +39,6 @@ RSpec.configure do |config|
39
39
 
40
40
  # Handles acton before rspec initialize
41
41
  config.before(:suite) do
42
- ActiveSupport::Deprecation.silenced = true
43
42
  DatabaseCleaner.clean_with(:truncation)
44
43
  end
45
44
 
@@ -51,8 +51,6 @@ RSpec.describe 'Arel' do
51
51
  context 'on default value' do
52
52
  let(:connection) { ActiveRecord::Base.connection }
53
53
 
54
- before(:context) { Torque::PostgreSQL.config.use_extended_defaults = true }
55
- after(:context) { Torque::PostgreSQL.config.use_extended_defaults = false }
56
54
  after { Author.reset_column_information }
57
55
 
58
56
  it 'does not break the change column default value method' do
@@ -76,7 +74,7 @@ RSpec.describe 'Arel' do
76
74
 
77
75
  expect do
78
76
  connection.create_enum(:samples, %i[a b c d])
79
- connection.add_column(:authors, :samples, :samples, array: true, default: value)
77
+ connection.add_column(:authors, :samples, :enum, enum_type: :samples, array: true, default: value)
80
78
  end.not_to raise_error
81
79
 
82
80
  expect(Author.new.samples).to eq(value)
@@ -84,7 +82,7 @@ RSpec.describe 'Arel' do
84
82
 
85
83
  it 'works with an array with enum values for an existing enum' do
86
84
  value = ['visitor', 'assistant']
87
- expect { connection.add_column(:authors, :roles, :roles, array: true, default: value) }.not_to raise_error
85
+ expect { connection.add_column(:authors, :roles, :enum, enum_type: :roles, array: true, default: value) }.not_to raise_error
88
86
  expect(Author.new.roles).to eq(value)
89
87
  end
90
88