diff --git a/.circleci/config.yml b/.circleci/config.yml index 6ae44ff4..8572f1dc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -70,7 +70,9 @@ workflows: matrix: parameters: ruby_version: ["ruby:2.6-buster", "ruby:2.7-buster", "ruby:3.0-buster"] - gemfile: ["gemfiles/rails_5_2.gemfile", "gemfiles/rails_6_0.gemfile", "gemfiles/rails_6_1.gemfile"] + gemfile: ["gemfiles/rails_5_2.gemfile", "gemfiles/rails_6_0.gemfile", "gemfiles/rails_6_1.gemfile", "gemfiles/rails_7_0.gemfile"] exclude: - ruby_version: "ruby:3.0-buster" gemfile: "gemfiles/rails_5_2.gemfile" + - ruby_version: "ruby:2.6-buster" + gemfile: "gemfiles/rails_7_0.gemfile" diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3650e125..ce2bf148 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -77,7 +77,6 @@ RSpec/DescribedClass: - 'spec/tenant_spec.rb' - 'spec/unit/elevators/host_hash_spec.rb' - 'spec/unit/migrator_spec.rb' - - 'spec/unit/reloader_spec.rb' # Offense count: 5 # Cop supports --auto-correct. @@ -128,7 +127,6 @@ RSpec/FilePath: - 'spec/unit/elevators/host_spec.rb' - 'spec/unit/elevators/subdomain_spec.rb' - 'spec/unit/migrator_spec.rb' - - 'spec/unit/reloader_spec.rb' # Offense count: 1 # Cop supports --auto-correct. @@ -159,7 +157,6 @@ RSpec/InstanceVariable: # Cop supports --auto-correct. RSpec/LeadingSubject: Exclude: - - 'spec/unit/reloader_spec.rb' # Offense count: 2 RSpec/LeakyConstantDeclaration: @@ -196,7 +193,6 @@ RSpec/NamedSubject: - 'spec/support/contexts.rb' - 'spec/support/requirements.rb' - 'spec/tenant_spec.rb' - - 'spec/unit/reloader_spec.rb' # Offense count: 24 RSpec/NestedGroups: @@ -217,7 +213,6 @@ RSpec/VerifiedDoubles: Exclude: - 'spec/integration/apartment_rake_integration_spec.rb' - 'spec/unit/elevators/first_subdomain_spec.rb' - - 'spec/unit/reloader_spec.rb' # Offense count: 17 Style/Documentation: @@ -233,7 +228,6 @@ Style/Documentation: - 'lib/apartment/migrator.rb' - 'lib/apartment/model.rb' - 'lib/apartment/railtie.rb' - - 'lib/apartment/reloader.rb' - 'lib/apartment/tasks/enhancements.rb' - 'lib/apartment/tasks/task_helper.rb' - 'lib/generators/apartment/install/install_generator.rb' diff --git a/.ruby-version b/.ruby-version index 2c9b4ef4..a603bb50 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.3 +2.7.5 diff --git a/Appraisals b/Appraisals index 5f55c5ac..43e788a4 100644 --- a/Appraisals +++ b/Appraisals @@ -1,29 +1,5 @@ # frozen_string_literal: true -appraise 'rails-5-0' do - gem 'rails', '~> 5.0.0' - platforms :ruby do - gem 'pg', '< 1.0.0' - end - platforms :jruby do - gem 'activerecord-jdbc-adapter', '~> 50.0' - gem 'activerecord-jdbcpostgresql-adapter', '~> 50.0' - gem 'activerecord-jdbcmysql-adapter', '~> 50.0' - end -end - -appraise 'rails-5-1' do - gem 'rails', '~> 5.1.0' - platforms :ruby do - gem 'pg', '< 1.0.0' - end - platforms :jruby do - gem 'activerecord-jdbc-adapter', '~> 51.0' - gem 'activerecord-jdbcpostgresql-adapter', '~> 51.0' - gem 'activerecord-jdbcmysql-adapter', '~> 51.0' - end -end - appraise 'rails-5-2' do gem 'rails', '~> 5.2.0' platforms :jruby do @@ -57,6 +33,18 @@ appraise 'rails-6-1' do end end +appraise 'rails-7-0' do + gem 'rails', '~> 7.0.0' + platforms :ruby do + gem 'sqlite3', '~> 1.4' + end + platforms :jruby do + gem 'activerecord-jdbc-adapter', '~> 61.0' + gem 'activerecord-jdbcpostgresql-adapter', '~> 61.0' + gem 'activerecord-jdbcmysql-adapter', '~> 61.0' + end +end + appraise 'rails-master' do gem 'rails', git: 'https://github.com/rails/rails.git' platforms :ruby do diff --git a/README.md b/README.md index 4ffc9231..58ecd407 100644 --- a/README.md +++ b/README.md @@ -344,7 +344,7 @@ Setting this configuration value to `false` will disable the schema presence che ```ruby Apartment.configure do |config| - tenant_presence_check = false + config.tenant_presence_check = false end ``` diff --git a/gemfiles/rails_4_2.gemfile b/gemfiles/rails_4_2.gemfile deleted file mode 100644 index 33048072..00000000 --- a/gemfiles/rails_4_2.gemfile +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -# This file was generated by Appraisal - -source 'http://rubygems.org' - -gem 'rails', '~> 4.2.0' - -group :local do - gem 'guard-rspec', '~> 4.2' - gem 'pry' -end - -platforms :ruby do - gem 'mysql2', '~> 0.4.0' - gem 'pg', '< 1.0.0' -end - -platforms :jruby do - gem 'activerecord-jdbc-adapter', '~> 1.3' - gem 'activerecord-jdbcmysql-adapter', '~> 1.3' - gem 'activerecord-jdbcpostgresql-adapter', '~> 1.3' -end - -gemspec path: '../' diff --git a/gemfiles/rails_5_0.gemfile b/gemfiles/rails_5_0.gemfile deleted file mode 100644 index 37dc042b..00000000 --- a/gemfiles/rails_5_0.gemfile +++ /dev/null @@ -1,23 +0,0 @@ -# This file was generated by Appraisal - -source "http://rubygems.org" - -gem "rails", "~> 5.0.0" -gem "rubocop" - -group :local do - gem "guard-rspec", "~> 4.2" - gem "pry" -end - -platforms :ruby do - gem "pg", "< 1.0.0" -end - -platforms :jruby do - gem "activerecord-jdbc-adapter", "~> 50.0" - gem "activerecord-jdbcpostgresql-adapter", "~> 50.0" - gem "activerecord-jdbcmysql-adapter", "~> 50.0" -end - -gemspec path: "../" diff --git a/gemfiles/rails_5_1.gemfile b/gemfiles/rails_5_1.gemfile deleted file mode 100644 index 59af05f8..00000000 --- a/gemfiles/rails_5_1.gemfile +++ /dev/null @@ -1,23 +0,0 @@ -# This file was generated by Appraisal - -source "http://rubygems.org" - -gem "rails", "~> 5.1.0" -gem "rubocop" - -group :local do - gem "guard-rspec", "~> 4.2" - gem "pry" -end - -platforms :ruby do - gem "pg", "< 1.0.0" -end - -platforms :jruby do - gem "activerecord-jdbc-adapter", "~> 51.0" - gem "activerecord-jdbcpostgresql-adapter", "~> 51.0" - gem "activerecord-jdbcmysql-adapter", "~> 51.0" -end - -gemspec path: "../" diff --git a/gemfiles/rails_5_2.gemfile b/gemfiles/rails_5_2.gemfile index 18d8952a..e50bd638 100644 --- a/gemfiles/rails_5_2.gemfile +++ b/gemfiles/rails_5_2.gemfile @@ -3,12 +3,6 @@ source "http://rubygems.org" gem "rails", "~> 5.2.0" -gem "rubocop" - -group :local do - gem "guard-rspec", "~> 4.2" - gem "pry" -end platforms :jruby do gem "activerecord-jdbc-adapter", "~> 52.0" diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile index 6d23e4aa..64778099 100644 --- a/gemfiles/rails_6_0.gemfile +++ b/gemfiles/rails_6_0.gemfile @@ -3,12 +3,6 @@ source "http://rubygems.org" gem "rails", "~> 6.0.0" -gem "rubocop" - -group :local do - gem "guard-rspec", "~> 4.2" - gem "pry" -end platforms :ruby do gem "sqlite3", "~> 1.4" diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index e6de4f27..ef48f142 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -3,12 +3,6 @@ source "http://rubygems.org" gem "rails", "~> 6.1.0" -gem "rubocop" - -group :local do - gem "guard-rspec", "~> 4.2" - gem "pry" -end platforms :ruby do gem "sqlite3", "~> 1.4" diff --git a/gemfiles/rails_7_0.gemfile b/gemfiles/rails_7_0.gemfile new file mode 100644 index 00000000..52256985 --- /dev/null +++ b/gemfiles/rails_7_0.gemfile @@ -0,0 +1,17 @@ +# This file was generated by Appraisal + +source "http://rubygems.org" + +gem "rails", "~> 7.0.0" + +platforms :ruby do + gem "sqlite3", "~> 1.4" +end + +platforms :jruby do + gem "activerecord-jdbc-adapter", "~> 61.0" + gem "activerecord-jdbcpostgresql-adapter", "~> 61.0" + gem "activerecord-jdbcmysql-adapter", "~> 61.0" +end + +gemspec path: "../" diff --git a/gemfiles/rails_master.gemfile b/gemfiles/rails_master.gemfile index 82ad9191..200ae4d1 100644 --- a/gemfiles/rails_master.gemfile +++ b/gemfiles/rails_master.gemfile @@ -3,21 +3,15 @@ source "http://rubygems.org" gem "rails", git: "https://github.com/rails/rails.git" -gem "rubocop" - -group :local do - gem "guard-rspec", "~> 4.2" - gem "pry" -end platforms :ruby do gem "sqlite3", "~> 1.4" end platforms :jruby do - gem "activerecord-jdbc-adapter", "~> 52.0" - gem "activerecord-jdbcpostgresql-adapter", "~> 52.0" - gem "activerecord-jdbcmysql-adapter", "~> 52.0" + gem "activerecord-jdbc-adapter", "~> 61.0" + gem "activerecord-jdbcpostgresql-adapter", "~> 61.0" + gem "activerecord-jdbcmysql-adapter", "~> 61.0" end gemspec path: "../" diff --git a/lib/apartment/active_record/postgresql_adapter.rb b/lib/apartment/active_record/postgresql_adapter.rb new file mode 100644 index 00000000..ef878111 --- /dev/null +++ b/lib/apartment/active_record/postgresql_adapter.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# rubocop:disable Style/ClassAndModuleChildren + +# NOTE: This patch is meant to remove any schema_prefix appart from the ones for +# excluded models. The schema_prefix would be resolved by apartment's setting +# of search path +module Apartment::PostgreSqlAdapterPatch + def default_sequence_name(table, _column) + res = super + schema_prefix = "#{Apartment::Tenant.current}." + default_tenant_prefix = "#{Apartment::Tenant.default_tenant}." + + # NOTE: Excluded models should always access the sequence from the default + # tenant schema + if excluded_model?(table) + res.sub!(schema_prefix, default_tenant_prefix) if schema_prefix != default_tenant_prefix + return res + end + + res.delete_prefix!(schema_prefix) if res&.starts_with?(schema_prefix) + + res + end + + private + + def excluded_model?(table) + Apartment.excluded_models.any? { |m| m.constantize.table_name == table } + end +end + +require 'active_record/connection_adapters/postgresql_adapter' + +# NOTE: inject this into postgresql adapters +class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter + include Apartment::PostgreSqlAdapterPatch +end +# rubocop:enable Style/ClassAndModuleChildren diff --git a/lib/apartment/adapters/postgresql_adapter.rb b/lib/apartment/adapters/postgresql_adapter.rb index 5d7f9e21..7b85aa51 100644 --- a/lib/apartment/adapters/postgresql_adapter.rb +++ b/lib/apartment/adapters/postgresql_adapter.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'apartment/adapters/abstract_adapter' +require 'apartment/active_record/postgresql_adapter' module Apartment module Tenant @@ -41,7 +42,6 @@ def default_tenant def reset @current = default_tenant Apartment.connection.schema_search_path = full_search_path - reset_sequence_names end def init @@ -81,9 +81,8 @@ def connect_to_new(tenant = nil) # there is a issue for prepared statement with changing search_path. # https://www.postgresql.org/docs/9.3/static/sql-prepare.html Apartment.connection.clear_cache! if postgresql_version < 90_300 - reset_sequence_names - rescue *rescuable_exceptions - raise TenantNotFound, "One of the following schema(s) is invalid: \"#{tenant}\" #{full_search_path}" + rescue *rescuable_exceptions => e + raise_schema_connect_to_new(tenant, e) end private @@ -130,29 +129,18 @@ def postgresql_version Apartment.connection.send(:postgresql_version) end - def reset_sequence_names - # sequence_name contains the schema, so it must be reset after switch - # There is `reset_sequence_name`, but that method actually goes to the database - # to find out the new name. Therefore, we do this hack to only unset the name, - # and it will be dynamically found the next time it is needed - descendants_to_unset = ActiveRecord::Base.descendants - .select { |c| c.instance_variable_defined?(:@sequence_name) } - .reject do |c| - c.instance_variable_defined?(:@explicit_sequence_name) && - c.instance_variable_get(:@explicit_sequence_name) - end - descendants_to_unset.each do |c| - # NOTE: due to this https://github.com/rails-on-services/apartment/issues/81 - # unreproduceable error we're checking before trying to remove it - c.remove_instance_variable :@sequence_name if c.instance_variable_defined?(:@sequence_name) - end - end - def schema_exists?(schemas) return true unless Apartment.tenant_presence_check Array(schemas).all? { |schema| Apartment.connection.schema_exists?(schema.to_s) } end + + def raise_schema_connect_to_new(tenant, exception) + raise TenantNotFound, <<~EXCEPTION_MESSAGE + Could not set search path to schemas, they may be invalid: "#{tenant}" #{full_search_path}. + Original error: #{exception.class}: #{exception} + EXCEPTION_MESSAGE + end end # Another Adapter for Postgresql when using schemas and SQL diff --git a/lib/apartment/console.rb b/lib/apartment/console.rb index 91721222..6cc3900d 100644 --- a/lib/apartment/console.rb +++ b/lib/apartment/console.rb @@ -1,21 +1,5 @@ # frozen_string_literal: true -# A workaround to get `reload!` to also call Apartment::Tenant.init -# This is unfortunate, but I haven't figured out how to hook into the reload process *after* files are reloaded - -# reloads the environment -# rubocop:disable Style/OptionalBooleanParameter -def reload!(print = true) - puts 'Reloading...' if print - - # This triggers the to_prepare callbacks - ActionDispatch::Callbacks.new(proc {}).call({}) - # Manually init Apartment again once classes are reloaded - Apartment::Tenant.init - true -end -# rubocop:enable Style/OptionalBooleanParameter - def st(schema_name = nil) if schema_name.nil? tenant_list.each { |t| puts t } diff --git a/lib/apartment/railtie.rb b/lib/apartment/railtie.rb index ba4e5728..efbe9c48 100644 --- a/lib/apartment/railtie.rb +++ b/lib/apartment/railtie.rb @@ -2,7 +2,6 @@ require 'rails' require 'apartment/tenant' -require 'apartment/reloader' module Apartment class Railtie < Rails::Railtie @@ -60,23 +59,5 @@ class Railtie < Rails::Railtie load 'tasks/apartment.rake' require 'apartment/tasks/enhancements' if Apartment.db_migrate_tenants end - - # - # The following initializers are a workaround to the fact that I can't properly hook into the rails reloader - # Note this is technically valid for any environment where cache_classes is false, for us, it's just development - # - if Rails.env.development? - - # Apartment::Reloader is middleware to initialize things properly on each request to dev - initializer 'apartment.init' do |app| - app.config.middleware.use Apartment::Reloader - end - - # Overrides reload! to also call Apartment::Tenant.init as well - # so that the reloaded classes have the proper table_names - console do - require 'apartment/console' - end - end end end diff --git a/lib/apartment/reloader.rb b/lib/apartment/reloader.rb deleted file mode 100644 index cd8b6861..00000000 --- a/lib/apartment/reloader.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module Apartment - class Reloader - # Middleware used in development to init Apartment for each request - # Necessary due to code reload (annoying). When models are reloaded, they no longer have the proper table_name - # That is prepended with the schema (if using postgresql schemas) - # I couldn't figure out how to properly hook into the Rails reload process *after* files are reloaded - # so I've used this in the meantime. - # - # Also see apartment/console for the re-definition of reload! that re-init's Apartment - # - def initialize(app) - @app = app - end - - def call(env) - Tenant.init - @app.call(env) - end - end -end diff --git a/lib/apartment/version.rb b/lib/apartment/version.rb index 31deaf81..4a24fa5e 100644 --- a/lib/apartment/version.rb +++ b/lib/apartment/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Apartment - VERSION = '2.10.0' + VERSION = '2.11.0' end diff --git a/ros-apartment.gemspec b/ros-apartment.gemspec index 169865c4..1f513d52 100644 --- a/ros-apartment.gemspec +++ b/ros-apartment.gemspec @@ -27,7 +27,7 @@ Gem::Specification.new do |s| s.homepage = 'https://github.com/rails-on-services/apartment' s.licenses = ['MIT'] - s.add_dependency 'activerecord', '>= 5.0.0', '< 6.2' + s.add_dependency 'activerecord', '>= 5.0.0', '< 7.1' s.add_dependency 'parallel', '< 2.0' s.add_dependency 'public_suffix', '>= 2.0.5', '< 5.0' s.add_dependency 'rack', '>= 1.3.6', '< 3.0' diff --git a/spec/adapters/jdbc_mysql_adapter_spec.rb b/spec/adapters/jdbc_mysql_adapter_spec.rb index b757c96b..de9c3860 100644 --- a/spec/adapters/jdbc_mysql_adapter_spec.rb +++ b/spec/adapters/jdbc_mysql_adapter_spec.rb @@ -6,7 +6,7 @@ require 'apartment/adapters/jdbc_mysql_adapter' describe Apartment::Adapters::JDBCMysqlAdapter, database: :mysql do - subject { Apartment::Tenant.jdbc_mysql_adapter config.symbolize_keys } + subject(:adapter) { Apartment::Tenant.adapter } def tenant_names ActiveRecord::Base.connection.execute('SELECT schema_name FROM information_schema.schemata').collect do |row| diff --git a/spec/adapters/jdbc_postgresql_adapter_spec.rb b/spec/adapters/jdbc_postgresql_adapter_spec.rb index d1deabf3..4db0eb8b 100644 --- a/spec/adapters/jdbc_postgresql_adapter_spec.rb +++ b/spec/adapters/jdbc_postgresql_adapter_spec.rb @@ -6,7 +6,7 @@ require 'apartment/adapters/jdbc_postgresql_adapter' describe Apartment::Adapters::JDBCPostgresqlAdapter, database: :postgresql do - subject { Apartment::Tenant.jdbc_postgresql_adapter config.symbolize_keys } + subject(:adapter) { Apartment::Tenant.adapter } it_behaves_like 'a generic apartment adapter callbacks' diff --git a/spec/adapters/mysql2_adapter_spec.rb b/spec/adapters/mysql2_adapter_spec.rb index fea994f4..6ff7c56c 100644 --- a/spec/adapters/mysql2_adapter_spec.rb +++ b/spec/adapters/mysql2_adapter_spec.rb @@ -6,7 +6,7 @@ describe Apartment::Adapters::Mysql2Adapter, database: :mysql do unless defined?(JRUBY_VERSION) - subject(:adapter) { Apartment::Tenant.mysql2_adapter config } + subject(:adapter) { Apartment::Tenant.adapter } def tenant_names ActiveRecord::Base.connection.execute('SELECT schema_name FROM information_schema.schemata').collect do |row| diff --git a/spec/adapters/postgresql_adapter_spec.rb b/spec/adapters/postgresql_adapter_spec.rb index 7944c862..981440f6 100644 --- a/spec/adapters/postgresql_adapter_spec.rb +++ b/spec/adapters/postgresql_adapter_spec.rb @@ -6,7 +6,7 @@ describe Apartment::Adapters::PostgresqlAdapter, database: :postgresql do unless defined?(JRUBY_VERSION) - subject { Apartment::Tenant.postgresql_adapter config } + subject { Apartment::Tenant.adapter } it_behaves_like 'a generic apartment adapter callbacks' diff --git a/spec/adapters/sqlite3_adapter_spec.rb b/spec/adapters/sqlite3_adapter_spec.rb index 1581a3e3..339cb38c 100644 --- a/spec/adapters/sqlite3_adapter_spec.rb +++ b/spec/adapters/sqlite3_adapter_spec.rb @@ -6,7 +6,7 @@ describe Apartment::Adapters::Sqlite3Adapter, database: :sqlite do unless defined?(JRUBY_VERSION) - subject { Apartment::Tenant.sqlite3_adapter config } + subject(:adapter) { Apartment::Tenant.adapter } it_behaves_like 'a generic apartment adapter callbacks' diff --git a/spec/examples/schema_adapter_examples.rb b/spec/examples/schema_adapter_examples.rb index 586dce62..70c71504 100644 --- a/spec/examples/schema_adapter_examples.rb +++ b/spec/examples/schema_adapter_examples.rb @@ -27,6 +27,9 @@ Apartment::Tenant.init expect(Company.table_name).to eq('public.companies') + expect(Company.sequence_name).to eq('public.companies_id_seq') + expect(User.table_name).to eq('users') + expect(User.sequence_name).to eq('users_id_seq') end context 'with a default_tenant', default_tenant: true do @@ -34,6 +37,9 @@ Apartment::Tenant.init expect(Company.table_name).to eq("#{default_tenant}.companies") + expect(Company.sequence_name).to eq("#{default_tenant}.companies_id_seq") + expect(User.table_name).to eq('users') + expect(User.sequence_name).to eq('users_id_seq') end it 'sets the search_path correctly' do @@ -116,15 +122,25 @@ end describe '#switch' do + before do + Apartment.configure do |config| + config.excluded_models = ['Company'] + end + end + + # rubocop:disable RSpec/MultipleExpectations it 'connects and resets' do subject.switch(schema1) do expect(connection.schema_search_path).to start_with %("#{schema1}") - expect(User.sequence_name).to eq "#{schema1}.#{User.table_name}_id_seq" + expect(User.sequence_name).to eq "#{User.table_name}_id_seq" + expect(Company.sequence_name).to eq "#{public_schema}.#{Company.table_name}_id_seq" end expect(connection.schema_search_path).to start_with %("#{public_schema}") - expect(User.sequence_name).to eq "#{public_schema}.#{User.table_name}_id_seq" + expect(User.sequence_name).to eq "#{User.table_name}_id_seq" + expect(Company.sequence_name).to eq "#{public_schema}.#{Company.table_name}_id_seq" end + # rubocop:enable RSpec/MultipleExpectations it 'allows a list of schemas' do subject.switch([schema1, schema2]) do diff --git a/spec/unit/reloader_spec.rb b/spec/unit/reloader_spec.rb deleted file mode 100644 index b194a46c..00000000 --- a/spec/unit/reloader_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Apartment::Reloader do - context 'when using postgresql schemas' do - before do - Apartment.configure do |config| - config.excluded_models = ['Company'] - config.use_schemas = true - end - Apartment::Tenant.reload!(config) - Company.reset_table_name # ensure we're clean - end - - subject { Apartment::Reloader.new(double('Rack::Application', call: nil)) } - - it 'initializes apartment when called' do - expect(Company.table_name).not_to include('public.') - subject.call(double('env')) - expect(Company.table_name).to include('public.') - end - end -end