ros-apartment 2.11.0 → 3.2.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +55 -8
  3. data/.ruby-version +1 -1
  4. data/Appraisals +106 -34
  5. data/CODE_OF_CONDUCT.md +71 -0
  6. data/Gemfile +15 -0
  7. data/README.md +52 -35
  8. data/Rakefile +40 -36
  9. data/{CHANGELOG.md → legacy_CHANGELOG.md} +5 -3
  10. data/lib/apartment/active_record/connection_handling.rb +18 -7
  11. data/lib/apartment/active_record/postgres/schema_dumper.rb +20 -0
  12. data/lib/apartment/active_record/postgresql_adapter.rb +23 -4
  13. data/lib/apartment/active_record/schema_migration.rb +1 -1
  14. data/lib/apartment/adapters/abstract_adapter.rb +1 -1
  15. data/lib/apartment/adapters/postgresql_adapter.rb +15 -19
  16. data/lib/apartment/adapters/trilogy_adapter.rb +29 -0
  17. data/lib/apartment/deprecation.rb +2 -5
  18. data/lib/apartment/log_subscriber.rb +21 -9
  19. data/lib/apartment/migrator.rb +6 -12
  20. data/lib/apartment/railtie.rb +6 -1
  21. data/lib/apartment/tasks/task_helper.rb +3 -1
  22. data/lib/apartment/version.rb +1 -1
  23. data/lib/apartment.rb +12 -16
  24. data/ros-apartment.gemspec +13 -32
  25. metadata +40 -246
  26. data/.circleci/config.yml +0 -78
  27. data/.github/ISSUE_TEMPLATE.md +0 -21
  28. data/.github/workflows/changelog.yml +0 -63
  29. data/.github/workflows/reviewdog.yml +0 -22
  30. data/.rubocop_todo.yml +0 -233
  31. data/.story_branch.yml +0 -5
  32. data/HISTORY.md +0 -496
  33. data/TODO.md +0 -50
  34. data/docker-compose.yml +0 -33
  35. data/gemfiles/rails_5_2.gemfile +0 -13
  36. data/gemfiles/rails_6_0.gemfile +0 -17
  37. data/gemfiles/rails_6_1.gemfile +0 -17
  38. data/gemfiles/rails_7_0.gemfile +0 -17
  39. data/gemfiles/rails_master.gemfile +0 -17
@@ -1,16 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActiveRecord
4
- # This is monkeypatching activerecord to ensure that whenever a new connection is established it
3
+ module ActiveRecord # :nodoc:
4
+ # This is monkeypatching Active Record to ensure that whenever a new connection is established it
5
5
  # switches to the same tenant as before the connection switching. This problem is more evident when
6
6
  # using read replica in Rails 6
7
7
  module ConnectionHandling
8
- def connected_to_with_tenant(database: nil, role: nil, prevent_writes: false, &blk)
9
- current_tenant = Apartment::Tenant.current
8
+ if ActiveRecord.version.release <= Gem::Version.new('6.2')
9
+ def connected_to_with_tenant(database: nil, role: nil, prevent_writes: false, &blk)
10
+ current_tenant = Apartment::Tenant.current
10
11
 
11
- connected_to_without_tenant(database: database, role: role, prevent_writes: prevent_writes) do
12
- Apartment::Tenant.switch!(current_tenant)
13
- yield(blk)
12
+ connected_to_without_tenant(database: database, role: role, prevent_writes: prevent_writes) do
13
+ Apartment::Tenant.switch!(current_tenant)
14
+ yield(blk)
15
+ end
16
+ end
17
+ else
18
+ def connected_to_with_tenant(role: nil, shard: nil, prevent_writes: false, &blk)
19
+ current_tenant = Apartment::Tenant.current
20
+
21
+ connected_to_without_tenant(role: role, shard: shard, prevent_writes: prevent_writes) do
22
+ Apartment::Tenant.switch!(current_tenant)
23
+ yield(blk)
24
+ end
14
25
  end
15
26
  end
16
27
 
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This patch prevents `create_schema` from being added to db/schema.rb as schemas are managed by Apartment
4
+ # not ActiveRecord like they would be in a vanilla Rails setup.
5
+
6
+ require 'active_record/connection_adapters/abstract/schema_dumper'
7
+ require 'active_record/connection_adapters/postgresql/schema_dumper'
8
+
9
+ module ActiveRecord
10
+ module ConnectionAdapters
11
+ module PostgreSQL
12
+ class SchemaDumper
13
+ alias _original_schemas schemas
14
+ def schemas(stream)
15
+ _original_schemas(stream) unless Apartment.use_schemas
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -8,23 +8,42 @@
8
8
  module Apartment::PostgreSqlAdapterPatch
9
9
  def default_sequence_name(table, _column)
10
10
  res = super
11
- schema_prefix = "#{Apartment::Tenant.current}."
12
- default_tenant_prefix = "#{Apartment::Tenant.default_tenant}."
11
+
12
+ # for JDBC driver, if rescued in super_method, trim leading and trailing quotes
13
+ res.delete!('"') if defined?(JRUBY_VERSION)
14
+
15
+ schema_prefix = "#{sequence_schema(res)}."
13
16
 
14
17
  # NOTE: Excluded models should always access the sequence from the default
15
18
  # tenant schema
16
19
  if excluded_model?(table)
17
- res.sub!(schema_prefix, default_tenant_prefix) if schema_prefix != default_tenant_prefix
20
+ default_tenant_prefix = "#{Apartment::Tenant.default_tenant}."
21
+
22
+ # Unless the res is already prefixed with the default_tenant_prefix
23
+ # we should delete the schema_prefix and add the default_tenant_prefix
24
+ unless res&.starts_with?(default_tenant_prefix)
25
+ res&.delete_prefix!(schema_prefix)
26
+ res = default_tenant_prefix + res
27
+ end
28
+
18
29
  return res
19
30
  end
20
31
 
21
- res.delete_prefix!(schema_prefix) if res&.starts_with?(schema_prefix)
32
+ # Delete the schema_prefix from the res if it is present
33
+ res&.delete_prefix!(schema_prefix)
22
34
 
23
35
  res
24
36
  end
25
37
 
26
38
  private
27
39
 
40
+ def sequence_schema(sequence_name)
41
+ current = Apartment::Tenant.current
42
+ return current unless current.is_a?(Array)
43
+
44
+ current.find { |schema| sequence_name.starts_with?("#{schema}.") }
45
+ end
46
+
28
47
  def excluded_model?(table)
29
48
  Apartment.excluded_models.any? { |m| m.constantize.table_name == table }
30
49
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecord
4
- class SchemaMigration < ActiveRecord::Base # :nodoc:
4
+ class SchemaMigration # :nodoc:
5
5
  class << self
6
6
  def table_exists?
7
7
  connection.table_exists?(table_name)
@@ -181,7 +181,7 @@ module Apartment
181
181
  query_cache_enabled = ActiveRecord::Base.connection.query_cache_enabled
182
182
 
183
183
  Apartment.establish_connection multi_tenantify(tenant)
184
- Apartment.connection.active? # call active? to manually check if this connection is valid
184
+ Apartment.connection.verify! # call active? to manually check if this connection is valid
185
185
 
186
186
  Apartment.connection.enable_query_cache! if query_cache_enabled
187
187
  rescue *rescuable_exceptions => e
@@ -76,11 +76,6 @@ module Apartment
76
76
 
77
77
  @current = tenant.is_a?(Array) ? tenant.map(&:to_s) : tenant.to_s
78
78
  Apartment.connection.schema_search_path = full_search_path
79
-
80
- # When the PostgreSQL version is < 9.3,
81
- # there is a issue for prepared statement with changing search_path.
82
- # https://www.postgresql.org/docs/9.3/static/sql-prepare.html
83
- Apartment.connection.clear_cache! if postgresql_version < 90_300
84
79
  rescue *rescuable_exceptions => e
85
80
  raise_schema_connect_to_new(tenant, e)
86
81
  end
@@ -151,8 +146,9 @@ module Apartment
151
146
  /SET row_security/i, # new in postgresql 9.5
152
147
  /SET idle_in_transaction_session_timeout/i, # new in postgresql 9.6
153
148
  /SET default_table_access_method/i, # new in postgresql 12
154
- /CREATE SCHEMA public/i,
155
- /COMMENT ON SCHEMA public/i
149
+ /CREATE SCHEMA/i,
150
+ /COMMENT ON SCHEMA/i,
151
+ /SET transaction_timeout/i, # new in postgresql 17
156
152
 
157
153
  ].freeze
158
154
 
@@ -194,15 +190,13 @@ module Apartment
194
190
  # @return {String} raw SQL contaning only postgres schema dump
195
191
  #
196
192
  def pg_dump_schema
197
- # Skip excluded tables? :/
198
- # excluded_tables =
199
- # collect_table_names(Apartment.excluded_models)
200
- # .map! {|t| "-T #{t}"}
201
- # .join(' ')
202
-
203
- # `pg_dump -s -x -O -n #{default_tenant} #{excluded_tables} #{dbname}`
204
-
205
- with_pg_env { `pg_dump -s -x -O -n #{default_tenant} #{dbname}` }
193
+ exclude_table =
194
+ if Apartment.pg_exclude_clone_tables
195
+ excluded_tables.map! { |t| "-T #{t}" }.join(' ')
196
+ else
197
+ ''
198
+ end
199
+ with_pg_env { `pg_dump -s -x -O -n #{default_tenant} #{dbname} #{exclude_table}` }
206
200
  end
207
201
 
208
202
  # Dump data from schema_migrations table
@@ -254,6 +248,8 @@ module Apartment
254
248
  sql.gsub(/#{default_tenant}\.\w*/) do |match|
255
249
  if Apartment.pg_excluded_names.any? { |name| match.include? name }
256
250
  match
251
+ elsif Apartment.pg_exclude_clone_tables && excluded_tables.any?(match)
252
+ match
257
253
  else
258
254
  match.gsub("#{default_tenant}.", %("#{current}".))
259
255
  end
@@ -266,10 +262,10 @@ module Apartment
266
262
  regexps.select { |c| input.match c }
267
263
  end
268
264
 
269
- # Collect table names from AR Models
265
+ # Convenience method for excluded table names
270
266
  #
271
- def collect_table_names(models)
272
- models.map do |m|
267
+ def excluded_tables
268
+ Apartment.excluded_models.map do |m|
273
269
  m.constantize.table_name
274
270
  end
275
271
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apartment/adapters/mysql2_adapter'
4
+
5
+ module Apartment
6
+ # Helper module to decide wether to use trilogy adapter or trilogy adapter with schemas
7
+ module Tenant
8
+ def self.trilogy_adapter(config)
9
+ if Apartment.use_schemas
10
+ Adapters::TrilogySchemaAdapter.new(config)
11
+ else
12
+ Adapters::TrilogyAdapter.new(config)
13
+ end
14
+ end
15
+ end
16
+
17
+ module Adapters
18
+ class TrilogyAdapter < Mysql2Adapter
19
+ protected
20
+
21
+ def rescue_from
22
+ Trilogy::Error
23
+ end
24
+ end
25
+
26
+ class TrilogySchemaAdapter < Mysql2SchemaAdapter
27
+ end
28
+ end
29
+ end
@@ -1,11 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_support/deprecation'
4
+ require_relative 'version'
4
5
 
5
6
  module Apartment
6
- module Deprecation
7
- def self.warn(message)
8
- ActiveSupport::Deprecation.warn message
9
- end
10
- end
7
+ DEPRECATOR = ActiveSupport::Deprecation.new(Apartment::VERSION, 'Apartment')
11
8
  end
@@ -8,26 +8,38 @@ module Apartment
8
8
  # NOTE: for some reason, if the method definition is not here, then the custom debug method is not called
9
9
  # rubocop:disable Lint/UselessMethodDefinition
10
10
  def sql(event)
11
- super(event)
11
+ super
12
12
  end
13
13
  # rubocop:enable Lint/UselessMethodDefinition
14
14
 
15
15
  private
16
16
 
17
- def debug(progname = nil, &block)
17
+ def debug(progname = nil, &blk)
18
18
  progname = " #{apartment_log}#{progname}" unless progname.nil?
19
19
 
20
- super(progname, &block)
20
+ super
21
21
  end
22
22
 
23
23
  def apartment_log
24
- database = color("[#{Apartment.connection.raw_connection.db}] ", ActiveSupport::LogSubscriber::MAGENTA, true)
25
- schema = nil
26
- unless Apartment.connection.schema_search_path.nil?
27
- schema = color("[#{Apartment.connection.schema_search_path.tr('"', '')}] ",
28
- ActiveSupport::LogSubscriber::YELLOW, true)
29
- end
24
+ database = color("[#{database_name}] ", ActiveSupport::LogSubscriber::MAGENTA, bold: true)
25
+ schema = current_search_path
26
+ schema = color("[#{schema.tr('"', '')}] ", ActiveSupport::LogSubscriber::YELLOW, bold: true) unless schema.nil?
30
27
  "#{database}#{schema}"
31
28
  end
29
+
30
+ def current_search_path
31
+ if Apartment.connection.respond_to?(:schema_search_path)
32
+ Apartment.connection.schema_search_path
33
+ else
34
+ Apartment::Tenant.current # all others
35
+ end
36
+ end
37
+
38
+ def database_name
39
+ db_name = Apartment.connection.raw_connection.try(:db) # PostgreSQL, PostGIS
40
+ db_name ||= Apartment.connection.raw_connection.try(:query_options)&.dig(:database) # Mysql
41
+ db_name ||= Apartment.connection.current_database # Failover
42
+ db_name
43
+ end
32
44
  end
33
45
  end
@@ -13,8 +13,8 @@ module Apartment
13
13
 
14
14
  migration_scope_block = ->(migration) { ENV['SCOPE'].blank? || (ENV['SCOPE'] == migration.scope) }
15
15
 
16
- if activerecord_below_5_2?
17
- ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, version, &migration_scope_block)
16
+ if ActiveRecord.version >= Gem::Version.new('7.2.0')
17
+ ActiveRecord::Base.connection_pool.migration_context.migrate(version, &migration_scope_block)
18
18
  else
19
19
  ActiveRecord::Base.connection.migration_context.migrate(version, &migration_scope_block)
20
20
  end
@@ -24,8 +24,8 @@ module Apartment
24
24
  # Migrate up/down to a specific version
25
25
  def run(direction, database, version)
26
26
  Tenant.switch(database) do
27
- if activerecord_below_5_2?
28
- ActiveRecord::Migrator.run(direction, ActiveRecord::Migrator.migrations_paths, version)
27
+ if ActiveRecord.version >= Gem::Version.new('7.2.0')
28
+ ActiveRecord::Base.connection_pool.migration_context.run(direction, version)
29
29
  else
30
30
  ActiveRecord::Base.connection.migration_context.run(direction, version)
31
31
  end
@@ -35,18 +35,12 @@ module Apartment
35
35
  # rollback latest migration `step` number of times
36
36
  def rollback(database, step = 1)
37
37
  Tenant.switch(database) do
38
- if activerecord_below_5_2?
39
- ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_paths, step)
38
+ if ActiveRecord.version >= Gem::Version.new('7.2.0')
39
+ ActiveRecord::Base.connection_pool.migration_context.rollback(step)
40
40
  else
41
41
  ActiveRecord::Base.connection.migration_context.rollback(step)
42
42
  end
43
43
  end
44
44
  end
45
-
46
- private
47
-
48
- def activerecord_below_5_2?
49
- ActiveRecord.version.release < Gem::Version.new('5.2.0')
50
- end
51
45
  end
52
46
  end
@@ -47,7 +47,12 @@ module Apartment
47
47
  config.after_initialize do
48
48
  # NOTE: Load the custom log subscriber if enabled
49
49
  if Apartment.active_record_log
50
- ActiveSupport::Notifications.unsubscribe 'sql.active_record'
50
+ ActiveSupport::Notifications.notifier.listeners_for('sql.active_record').each do |listener|
51
+ next unless listener.instance_variable_get('@delegate').is_a?(ActiveRecord::LogSubscriber)
52
+
53
+ ActiveSupport::Notifications.unsubscribe listener
54
+ end
55
+
51
56
  Apartment::LogSubscriber.attach_to :active_record
52
57
  end
53
58
  end
@@ -4,7 +4,9 @@ module Apartment
4
4
  module TaskHelper
5
5
  def self.each_tenant(&block)
6
6
  Parallel.each(tenants_without_default, in_threads: Apartment.parallel_migration_threads) do |tenant|
7
- block.call(tenant)
7
+ Rails.application.executor.wrap do
8
+ block.call(tenant)
9
+ end
8
10
  end
9
11
  end
10
12
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Apartment
4
- VERSION = '2.11.0'
4
+ VERSION = '3.2.0'
5
5
  end
data/lib/apartment.rb CHANGED
@@ -5,16 +5,15 @@ require 'active_support/core_ext/object/blank'
5
5
  require 'forwardable'
6
6
  require 'active_record'
7
7
  require 'apartment/tenant'
8
+ require 'apartment/deprecation'
8
9
 
9
10
  require_relative 'apartment/log_subscriber'
11
+ require_relative 'apartment/active_record/connection_handling'
12
+ require_relative 'apartment/active_record/schema_migration'
13
+ require_relative 'apartment/active_record/internal_metadata'
10
14
 
11
- if ActiveRecord.version.release >= Gem::Version.new('6.0')
12
- require_relative 'apartment/active_record/connection_handling'
13
- end
14
-
15
- if ActiveRecord.version.release >= Gem::Version.new('6.1')
16
- require_relative 'apartment/active_record/schema_migration'
17
- require_relative 'apartment/active_record/internal_metadata'
15
+ if ActiveRecord.version.release >= Gem::Version.new('7.1')
16
+ require_relative 'apartment/active_record/postgres/schema_dumper'
18
17
  end
19
18
 
20
19
  # Apartment main definitions
@@ -23,7 +22,8 @@ module Apartment
23
22
  extend Forwardable
24
23
 
25
24
  ACCESSOR_METHODS = %i[use_schemas use_sql seed_after_create prepend_environment default_tenant
26
- append_environment with_multi_server_setup tenant_presence_check active_record_log].freeze
25
+ append_environment with_multi_server_setup tenant_presence_check
26
+ active_record_log pg_exclude_clone_tables].freeze
27
27
 
28
28
  WRITER_METHODS = %i[tenant_names database_schema_file excluded_models
29
29
  persistent_schemas connection_class
@@ -33,14 +33,10 @@ module Apartment
33
33
  attr_accessor(*ACCESSOR_METHODS)
34
34
  attr_writer(*WRITER_METHODS)
35
35
 
36
- if ActiveRecord.version.release >= Gem::Version.new('6.1')
37
- def_delegators :connection_class, :connection, :connection_db_config, :establish_connection
36
+ def_delegators :connection_class, :connection, :connection_db_config, :establish_connection
38
37
 
39
- def connection_config
40
- connection_db_config.configuration_hash
41
- end
42
- else
43
- def_delegators :connection_class, :connection, :connection_config, :establish_connection
38
+ def connection_config
39
+ connection_db_config.configuration_hash
44
40
  end
45
41
 
46
42
  # configure apartment with available options
@@ -57,7 +53,7 @@ module Apartment
57
53
  end
58
54
 
59
55
  def tld_length=(_)
60
- Apartment::Deprecation.warn('`config.tld_length` have no effect because it was removed in https://github.com/influitive/apartment/pull/309')
56
+ Apartment::DEPRECATOR.warn('`config.tld_length` have no effect because it was removed in https://github.com/influitive/apartment/pull/309')
61
57
  end
62
58
 
63
59
  def db_config_for(tenant)
@@ -7,53 +7,34 @@ Gem::Specification.new do |s|
7
7
  s.name = 'ros-apartment'
8
8
  s.version = Apartment::VERSION
9
9
 
10
- s.authors = ['Ryan Brunner', 'Brad Robertson', 'Rui Baltazar']
10
+ s.authors = ['Ryan Brunner', 'Brad Robertson', 'Rui Baltazar', 'Mauricio Novelo']
11
11
  s.summary = 'A Ruby gem for managing database multitenancy. Apartment Gem drop in replacement'
12
12
  s.description = 'Apartment allows Rack applications to deal with database multitenancy through ActiveRecord'
13
- s.email = ['ryan@influitive.com', 'brad@influitive.com', 'rui.p.baltazar@gmail.com']
13
+ s.email = ['ryan@influitive.com', 'brad@influitive.com', 'rui.p.baltazar@gmail.com', 'mauricio@campusesp.com']
14
14
  # Specify which files should be added to the gem when it is released.
15
15
  # The `git ls-files -z` loads the files in the RubyGem that have been
16
16
  # added into git.
17
17
  s.files = Dir.chdir(File.expand_path(__dir__)) do
18
18
  `git ls-files -z`.split("\x0").reject do |f|
19
19
  # NOTE: ignore all test related
20
- f.match(%r{^(test|spec|features|documentation)/})
20
+ f.match(%r{^(test|spec|features|documentation|gemfiles|.github)/})
21
21
  end
22
22
  end
23
23
  s.executables = s.files.grep(%r{^bin/}).map { |f| File.basename(f) }
24
- s.test_files = s.files.grep(%r{^(test|spec|features)/})
25
24
  s.require_paths = ['lib']
26
25
 
27
26
  s.homepage = 'https://github.com/rails-on-services/apartment'
28
27
  s.licenses = ['MIT']
28
+ s.metadata = {
29
+ 'github_repo' => 'ssh://github.com/rails-on-services/apartment',
30
+ 'rubygems_mfa_required' => 'true',
31
+ }
29
32
 
30
- s.add_dependency 'activerecord', '>= 5.0.0', '< 7.1'
31
- s.add_dependency 'parallel', '< 2.0'
32
- s.add_dependency 'public_suffix', '>= 2.0.5', '< 5.0'
33
- s.add_dependency 'rack', '>= 1.3.6', '< 3.0'
33
+ s.required_ruby_version = '>= 3.1'
34
34
 
35
- s.add_development_dependency 'appraisal', '~> 2.2'
36
- s.add_development_dependency 'bundler', '>= 1.3', '< 3.0'
37
- s.add_development_dependency 'guard-rspec', '~> 4.2'
38
- s.add_development_dependency 'pry'
39
- s.add_development_dependency 'rake', '~> 13.0'
40
- s.add_development_dependency 'rspec', '~> 3.4'
41
- s.add_development_dependency 'rspec_junit_formatter'
42
- s.add_development_dependency 'rspec-rails', '~> 3.4'
43
- s.add_development_dependency 'rubocop', '~> 0.93'
44
- s.add_development_dependency 'rubocop-performance', '~> 1.10'
45
- s.add_development_dependency 'rubocop-rails', '~> 2.1'
46
- s.add_development_dependency 'rubocop-rspec', '~> 1.44'
47
-
48
- if defined?(JRUBY_VERSION)
49
- s.add_development_dependency 'activerecord-jdbc-adapter'
50
- s.add_development_dependency 'activerecord-jdbcmysql-adapter'
51
- s.add_development_dependency 'activerecord-jdbcpostgresql-adapter'
52
- s.add_development_dependency 'jdbc-mysql'
53
- s.add_development_dependency 'jdbc-postgres'
54
- else
55
- s.add_development_dependency 'mysql2', '~> 0.5'
56
- s.add_development_dependency 'pg', '~> 1.2'
57
- s.add_development_dependency 'sqlite3', '~> 1.3.6'
58
- end
35
+ s.add_dependency('activerecord', '>= 6.1.0', '< 8.1')
36
+ s.add_dependency('activesupport', '>= 6.1.0', '< 8.1')
37
+ s.add_dependency('parallel', '< 2.0')
38
+ s.add_dependency('public_suffix', '>= 2.0.5', '<= 6.0.1')
39
+ s.add_dependency('rack', '>= 1.3.6', '< 4.0')
59
40
  end