activerecord-import 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +32 -0
  3. data/.rubocop.yml +49 -0
  4. data/.rubocop_todo.yml +36 -0
  5. data/.travis.yml +74 -0
  6. data/Brewfile +3 -0
  7. data/CHANGELOG.md +430 -0
  8. data/Gemfile +59 -0
  9. data/LICENSE +56 -0
  10. data/README.markdown +619 -0
  11. data/Rakefile +68 -0
  12. data/activerecord-import.gemspec +23 -0
  13. data/benchmarks/README +32 -0
  14. data/benchmarks/benchmark.rb +68 -0
  15. data/benchmarks/lib/base.rb +138 -0
  16. data/benchmarks/lib/cli_parser.rb +107 -0
  17. data/benchmarks/lib/float.rb +15 -0
  18. data/benchmarks/lib/mysql2_benchmark.rb +19 -0
  19. data/benchmarks/lib/output_to_csv.rb +19 -0
  20. data/benchmarks/lib/output_to_html.rb +64 -0
  21. data/benchmarks/models/test_innodb.rb +3 -0
  22. data/benchmarks/models/test_memory.rb +3 -0
  23. data/benchmarks/models/test_myisam.rb +3 -0
  24. data/benchmarks/schema/mysql_schema.rb +16 -0
  25. data/gemfiles/3.2.gemfile +2 -0
  26. data/gemfiles/4.0.gemfile +2 -0
  27. data/gemfiles/4.1.gemfile +2 -0
  28. data/gemfiles/4.2.gemfile +2 -0
  29. data/gemfiles/5.0.gemfile +2 -0
  30. data/gemfiles/5.1.gemfile +2 -0
  31. data/gemfiles/5.2.gemfile +2 -0
  32. data/gemfiles/6.0.gemfile +1 -0
  33. data/gemfiles/6.1.gemfile +1 -0
  34. data/lib/activerecord-import.rb +6 -0
  35. data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +9 -0
  36. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -0
  37. data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +6 -0
  38. data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +6 -0
  39. data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +6 -0
  40. data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +6 -0
  41. data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +7 -0
  42. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +6 -0
  43. data/lib/activerecord-import/adapters/abstract_adapter.rb +66 -0
  44. data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +5 -0
  45. data/lib/activerecord-import/adapters/mysql2_adapter.rb +5 -0
  46. data/lib/activerecord-import/adapters/mysql_adapter.rb +129 -0
  47. data/lib/activerecord-import/adapters/postgresql_adapter.rb +217 -0
  48. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +180 -0
  49. data/lib/activerecord-import/base.rb +43 -0
  50. data/lib/activerecord-import/import.rb +1059 -0
  51. data/lib/activerecord-import/mysql2.rb +7 -0
  52. data/lib/activerecord-import/postgresql.rb +7 -0
  53. data/lib/activerecord-import/sqlite3.rb +7 -0
  54. data/lib/activerecord-import/synchronize.rb +66 -0
  55. data/lib/activerecord-import/value_sets_parser.rb +77 -0
  56. data/lib/activerecord-import/version.rb +5 -0
  57. data/test/adapters/jdbcmysql.rb +1 -0
  58. data/test/adapters/jdbcpostgresql.rb +1 -0
  59. data/test/adapters/jdbcsqlite3.rb +1 -0
  60. data/test/adapters/makara_postgis.rb +1 -0
  61. data/test/adapters/mysql2.rb +1 -0
  62. data/test/adapters/mysql2_makara.rb +1 -0
  63. data/test/adapters/mysql2spatial.rb +1 -0
  64. data/test/adapters/postgis.rb +1 -0
  65. data/test/adapters/postgresql.rb +1 -0
  66. data/test/adapters/postgresql_makara.rb +1 -0
  67. data/test/adapters/seamless_database_pool.rb +1 -0
  68. data/test/adapters/spatialite.rb +1 -0
  69. data/test/adapters/sqlite3.rb +1 -0
  70. data/test/database.yml.sample +52 -0
  71. data/test/import_test.rb +903 -0
  72. data/test/jdbcmysql/import_test.rb +5 -0
  73. data/test/jdbcpostgresql/import_test.rb +4 -0
  74. data/test/jdbcsqlite3/import_test.rb +4 -0
  75. data/test/makara_postgis/import_test.rb +8 -0
  76. data/test/models/account.rb +3 -0
  77. data/test/models/alarm.rb +2 -0
  78. data/test/models/bike_maker.rb +7 -0
  79. data/test/models/book.rb +9 -0
  80. data/test/models/car.rb +3 -0
  81. data/test/models/chapter.rb +4 -0
  82. data/test/models/dictionary.rb +4 -0
  83. data/test/models/discount.rb +3 -0
  84. data/test/models/end_note.rb +4 -0
  85. data/test/models/group.rb +3 -0
  86. data/test/models/promotion.rb +3 -0
  87. data/test/models/question.rb +3 -0
  88. data/test/models/rule.rb +3 -0
  89. data/test/models/tag.rb +4 -0
  90. data/test/models/topic.rb +23 -0
  91. data/test/models/user.rb +3 -0
  92. data/test/models/user_token.rb +4 -0
  93. data/test/models/vendor.rb +7 -0
  94. data/test/models/widget.rb +24 -0
  95. data/test/mysql2/import_test.rb +5 -0
  96. data/test/mysql2_makara/import_test.rb +6 -0
  97. data/test/mysqlspatial2/import_test.rb +6 -0
  98. data/test/postgis/import_test.rb +8 -0
  99. data/test/postgresql/import_test.rb +4 -0
  100. data/test/schema/generic_schema.rb +194 -0
  101. data/test/schema/jdbcpostgresql_schema.rb +1 -0
  102. data/test/schema/mysql2_schema.rb +19 -0
  103. data/test/schema/postgis_schema.rb +1 -0
  104. data/test/schema/postgresql_schema.rb +47 -0
  105. data/test/schema/sqlite3_schema.rb +13 -0
  106. data/test/schema/version.rb +10 -0
  107. data/test/sqlite3/import_test.rb +4 -0
  108. data/test/support/active_support/test_case_extensions.rb +75 -0
  109. data/test/support/assertions.rb +73 -0
  110. data/test/support/factories.rb +64 -0
  111. data/test/support/generate.rb +29 -0
  112. data/test/support/mysql/import_examples.rb +98 -0
  113. data/test/support/postgresql/import_examples.rb +563 -0
  114. data/test/support/shared_examples/on_duplicate_key_ignore.rb +43 -0
  115. data/test/support/shared_examples/on_duplicate_key_update.rb +368 -0
  116. data/test/support/shared_examples/recursive_import.rb +216 -0
  117. data/test/support/sqlite3/import_examples.rb +231 -0
  118. data/test/synchronize_test.rb +41 -0
  119. data/test/test_helper.rb +75 -0
  120. data/test/travis/database.yml +66 -0
  121. data/test/value_sets_bytes_parser_test.rb +104 -0
  122. data/test/value_sets_records_parser_test.rb +32 -0
  123. metadata +259 -0
@@ -0,0 +1,5 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+ require File.expand_path(File.dirname(__FILE__) + '/../support/assertions')
3
+ require File.expand_path(File.dirname(__FILE__) + '/../support/mysql/import_examples')
4
+
5
+ should_support_mysql_import_functionality
@@ -0,0 +1,4 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+ require File.expand_path(File.dirname(__FILE__) + '/../support/postgresql/import_examples')
3
+
4
+ should_support_postgresql_import_functionality
@@ -0,0 +1,4 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+ require File.expand_path(File.dirname(__FILE__) + '/../support/sqlite3/import_examples')
3
+
4
+ should_support_sqlite3_import_functionality
@@ -0,0 +1,8 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+ require File.expand_path(File.dirname(__FILE__) + '/../support/postgresql/import_examples')
3
+
4
+ should_support_postgresql_import_functionality
5
+
6
+ if ActiveRecord::Base.connection.supports_on_duplicate_key_update?
7
+ should_support_postgresql_upsert_functionality
8
+ end
@@ -0,0 +1,3 @@
1
+ class Account < ActiveRecord::Base
2
+ self.locking_column = :lock
3
+ end
@@ -0,0 +1,2 @@
1
+ class Alarm < ActiveRecord::Base
2
+ end
@@ -0,0 +1,7 @@
1
+ module Bike
2
+ def self.table_name_prefix
3
+ 'bike_'
4
+ end
5
+ class Maker < ActiveRecord::Base
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ class Book < ActiveRecord::Base
2
+ belongs_to :topic, inverse_of: :books
3
+ belongs_to :tag, foreign_key: [:tag_id, :parent_id]
4
+
5
+ has_many :chapters, inverse_of: :book
6
+ has_many :discounts, as: :discountable
7
+ has_many :end_notes, inverse_of: :book
8
+ enum status: [:draft, :published] if ENV['AR_VERSION'].to_f >= 4.1
9
+ end
@@ -0,0 +1,3 @@
1
+ class Car < ActiveRecord::Base
2
+ self.primary_key = :Name
3
+ end
@@ -0,0 +1,4 @@
1
+ class Chapter < ActiveRecord::Base
2
+ belongs_to :book, inverse_of: :chapters
3
+ validates :title, presence: true
4
+ end
@@ -0,0 +1,4 @@
1
+ require_relative 'book'
2
+
3
+ class Dictionary < Book
4
+ end
@@ -0,0 +1,3 @@
1
+ class Discount < ActiveRecord::Base
2
+ belongs_to :discountable, polymorphic: true
3
+ end
@@ -0,0 +1,4 @@
1
+ class EndNote < ActiveRecord::Base
2
+ belongs_to :book, inverse_of: :end_notes
3
+ validates :note, presence: true
4
+ end
@@ -0,0 +1,3 @@
1
+ class Group < ActiveRecord::Base
2
+ self.table_name = 'group'
3
+ end
@@ -0,0 +1,3 @@
1
+ class Promotion < ActiveRecord::Base
2
+ self.primary_key = :promotion_id
3
+ end
@@ -0,0 +1,3 @@
1
+ class Question < ActiveRecord::Base
2
+ has_one :rule
3
+ end
@@ -0,0 +1,3 @@
1
+ class Rule < ActiveRecord::Base
2
+ belongs_to :question
3
+ end
@@ -0,0 +1,4 @@
1
+ class Tag < ActiveRecord::Base
2
+ self.primary_keys = :tag_id, :publisher_id unless ENV["SKIP_COMPOSITE_PK"]
3
+ has_many :books, inverse_of: :tag
4
+ end
@@ -0,0 +1,23 @@
1
+ class Topic < ActiveRecord::Base
2
+ validates_presence_of :author_name
3
+ validates :title, numericality: { only_integer: true }, on: :context_test
4
+ validates :title, uniqueness: true
5
+ validates :content, uniqueness: true
6
+ validates :word_count, numericality: { greater_than: 0 }, if: :content?
7
+
8
+ validate -> { errors.add(:title, :validate_failed) if title == 'validate_failed' }
9
+ before_validation -> { errors.add(:title, :invalid) if title == 'invalid' }
10
+
11
+ has_many :books, inverse_of: :topic
12
+ belongs_to :parent, class_name: "Topic"
13
+
14
+ composed_of :description, mapping: [%w(title title), %w(author_name author_name)], allow_nil: true, class_name: "TopicDescription"
15
+
16
+ default_scope { where(approved: true) }
17
+
18
+ private
19
+
20
+ def word_count
21
+ @word_count ||= content.to_s.scan(/\w+/).count
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ class User < ActiveRecord::Base
2
+ has_many :user_tokens, primary_key: :name, foreign_key: :user_name
3
+ end
@@ -0,0 +1,4 @@
1
+ class UserToken < ActiveRecord::Base
2
+ belongs_to :user, primary_key: :name, foreign_key: :user_name
3
+ validates :user, presence: true
4
+ end
@@ -0,0 +1,7 @@
1
+ class Vendor < ActiveRecord::Base
2
+ store :preferences, accessors: [:color], coder: JSON
3
+
4
+ store_accessor :data, :size
5
+ store_accessor :config, :contact
6
+ store_accessor :settings, :charge_code
7
+ end
@@ -0,0 +1,24 @@
1
+ class CustomCoder
2
+ def load(value)
3
+ if value.nil?
4
+ {}
5
+ else
6
+ YAML.load(value)
7
+ end
8
+ end
9
+
10
+ def dump(value)
11
+ YAML.dump(value)
12
+ end
13
+ end
14
+
15
+ class Widget < ActiveRecord::Base
16
+ self.primary_key = :w_id
17
+
18
+ default_scope -> { where(active: true) }
19
+
20
+ serialize :data, Hash
21
+ serialize :json_data, JSON
22
+ serialize :unspecified_data
23
+ serialize :custom_data, CustomCoder.new
24
+ end
@@ -0,0 +1,5 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+ require File.expand_path(File.dirname(__FILE__) + '/../support/assertions')
3
+ require File.expand_path(File.dirname(__FILE__) + '/../support/mysql/import_examples')
4
+
5
+ should_support_mysql_import_functionality
@@ -0,0 +1,6 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/../support/assertions')
4
+ require File.expand_path(File.dirname(__FILE__) + '/../support/mysql/import_examples')
5
+
6
+ should_support_mysql_import_functionality
@@ -0,0 +1,6 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/../support/assertions')
4
+ require File.expand_path(File.dirname(__FILE__) + '/../support/mysql/import_examples')
5
+
6
+ should_support_mysql_import_functionality
@@ -0,0 +1,8 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+ require File.expand_path(File.dirname(__FILE__) + '/../support/postgresql/import_examples')
3
+
4
+ should_support_postgresql_import_functionality
5
+
6
+ if ActiveRecord::Base.connection.supports_on_duplicate_key_update?
7
+ should_support_postgresql_upsert_functionality
8
+ end
@@ -0,0 +1,4 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+ require File.expand_path(File.dirname(__FILE__) + '/../support/postgresql/import_examples')
3
+
4
+ should_support_postgresql_import_functionality
@@ -0,0 +1,194 @@
1
+ ActiveRecord::Schema.define do
2
+ create_table :schema_info, force: :cascade do |t|
3
+ t.integer :version, unique: true
4
+ end
5
+ SchemaInfo.create version: SchemaInfo::VERSION
6
+
7
+ create_table :group, force: :cascade do |t|
8
+ t.string :order
9
+ t.timestamps null: true
10
+ end
11
+
12
+ create_table :topics, force: :cascade do |t|
13
+ t.string :title, null: false
14
+ t.string :author_name
15
+ t.string :author_email_address
16
+ t.datetime :written_on
17
+ t.time :bonus_time
18
+ t.datetime :last_read
19
+ t.text :content
20
+ t.boolean :approved, default: '1'
21
+ t.integer :replies_count
22
+ t.integer :parent_id
23
+ t.string :type
24
+ t.datetime :created_at
25
+ t.datetime :created_on
26
+ t.datetime :updated_at
27
+ t.datetime :updated_on
28
+ end
29
+
30
+ create_table :projects, force: :cascade do |t|
31
+ t.string :name
32
+ t.string :type
33
+ end
34
+
35
+ create_table :developers, force: :cascade do |t|
36
+ t.string :name
37
+ t.integer :salary, default: '70000'
38
+ t.datetime :created_at
39
+ t.integer :team_id
40
+ t.datetime :updated_at
41
+ end
42
+
43
+ create_table :addresses, force: :cascade do |t|
44
+ t.string :address
45
+ t.string :city
46
+ t.string :state
47
+ t.string :zip
48
+ t.integer :developer_id
49
+ end
50
+
51
+ create_table :teams, force: :cascade do |t|
52
+ t.string :name
53
+ end
54
+
55
+ create_table :books, force: :cascade do |t|
56
+ t.string :title, null: false
57
+ t.string :publisher, null: false, default: 'Default Publisher'
58
+ t.string :author_name, null: false
59
+ t.datetime :created_at
60
+ t.datetime :created_on
61
+ t.datetime :updated_at
62
+ t.datetime :updated_on
63
+ t.date :publish_date
64
+ t.integer :topic_id
65
+ t.integer :tag_id
66
+ t.integer :publisher_id
67
+ t.boolean :for_sale, default: true
68
+ t.integer :status, default: 0
69
+ t.string :type
70
+ end
71
+
72
+ create_table :chapters, force: :cascade do |t|
73
+ t.string :title
74
+ t.integer :book_id, null: false
75
+ t.datetime :created_at
76
+ t.datetime :updated_at
77
+ end
78
+
79
+ create_table :end_notes, primary_key: :end_note_id, force: :cascade do |t|
80
+ t.string :note
81
+ t.integer :book_id, null: false
82
+ t.datetime :created_at
83
+ t.datetime :updated_at
84
+ end
85
+
86
+ create_table :languages, force: :cascade do |t|
87
+ t.string :name
88
+ t.integer :developer_id
89
+ end
90
+
91
+ create_table :shopping_carts, force: :cascade do |t|
92
+ t.string :name, null: true
93
+ t.datetime :created_at
94
+ t.datetime :updated_at
95
+ end
96
+
97
+ create_table :cart_items, force: :cascade do |t|
98
+ t.string :shopping_cart_id, null: false
99
+ t.string :book_id, null: false
100
+ t.integer :copies, default: 1
101
+ t.datetime :created_at
102
+ t.datetime :updated_at
103
+ end
104
+
105
+ add_index :cart_items, [:shopping_cart_id, :book_id], unique: true, name: 'uk_shopping_cart_books'
106
+
107
+ create_table :animals, force: :cascade do |t|
108
+ t.string :name, null: false
109
+ t.string :size, default: nil
110
+ t.datetime :created_at
111
+ t.datetime :updated_at
112
+ end
113
+
114
+ add_index :animals, [:name], unique: true, name: 'uk_animals'
115
+
116
+ create_table :widgets, id: false, force: :cascade do |t|
117
+ t.integer :w_id, primary_key: true
118
+ t.boolean :active, default: false
119
+ t.text :data
120
+ t.text :json_data
121
+ t.text :unspecified_data
122
+ t.text :custom_data
123
+ end
124
+
125
+ create_table :promotions, primary_key: :promotion_id, force: :cascade do |t|
126
+ t.string :code
127
+ t.string :description
128
+ t.decimal :discount
129
+ end
130
+
131
+ add_index :promotions, [:code], unique: true, name: 'uk_code'
132
+
133
+ create_table :discounts, force: :cascade do |t|
134
+ t.decimal :amount
135
+ t.integer :discountable_id
136
+ t.string :discountable_type
137
+ end
138
+
139
+ create_table :rules, id: false, force: :cascade do |t|
140
+ t.integer :id
141
+ t.string :condition_text
142
+ t.integer :question_id
143
+ end
144
+
145
+ create_table :questions, force: :cascade do |t|
146
+ t.string :body
147
+ end
148
+
149
+ create_table :vendors, force: :cascade do |t|
150
+ t.string :name, null: true
151
+ t.text :preferences
152
+ t.text :data
153
+ t.text :config
154
+ t.text :settings
155
+ end
156
+
157
+ create_table :cars, id: false, force: :cascade do |t|
158
+ t.string :Name, null: true
159
+ t.string :Features
160
+ end
161
+
162
+ create_table :users, force: :cascade do |t|
163
+ t.string :name, null: false
164
+ t.integer :lock_version, null: false, default: 0
165
+ end
166
+
167
+ create_table :user_tokens, force: :cascade do |t|
168
+ t.string :user_name, null: false
169
+ t.string :token, null: false
170
+ end
171
+
172
+ create_table :accounts, force: :cascade do |t|
173
+ t.string :name, null: false
174
+ t.integer :lock, null: false, default: 0
175
+ end
176
+
177
+ create_table :bike_makers, force: :cascade do |t|
178
+ t.string :name, null: false
179
+ t.integer :lock_version, null: false, default: 0
180
+ end
181
+
182
+ add_index :cars, :Name, unique: true
183
+
184
+ unless ENV["SKIP_COMPOSITE_PK"]
185
+ execute %(
186
+ CREATE TABLE IF NOT EXISTS tags (
187
+ tag_id INT NOT NULL,
188
+ publisher_id INT NOT NULL,
189
+ tag VARCHAR(50),
190
+ PRIMARY KEY (tag_id, publisher_id)
191
+ );
192
+ ).split.join(' ').strip
193
+ end
194
+ end
@@ -0,0 +1 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/postgresql_schema')
@@ -0,0 +1,19 @@
1
+ ActiveRecord::Schema.define do
2
+ create_table :books, force: :cascade do |t|
3
+ t.string :title, null: false
4
+ t.virtual :upper_title, type: :string, as: "upper(`title`)" if t.respond_to?(:virtual)
5
+ t.string :publisher, null: false, default: 'Default Publisher'
6
+ t.string :author_name, null: false
7
+ t.datetime :created_at
8
+ t.datetime :created_on
9
+ t.datetime :updated_at
10
+ t.datetime :updated_on
11
+ t.date :publish_date
12
+ t.integer :topic_id
13
+ t.integer :tag_id
14
+ t.integer :publisher_id
15
+ t.boolean :for_sale, default: true
16
+ t.integer :status, default: 0
17
+ t.string :type
18
+ end
19
+ end
@@ -0,0 +1 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/postgresql_schema')
@@ -0,0 +1,47 @@
1
+ ActiveRecord::Schema.define do
2
+ execute('CREATE extension IF NOT EXISTS "hstore";')
3
+ execute('CREATE extension IF NOT EXISTS "pgcrypto";')
4
+ execute('CREATE extension IF NOT EXISTS "uuid-ossp";')
5
+
6
+ create_table :vendors, id: :uuid, force: :cascade do |t|
7
+ t.string :name, null: true
8
+ t.text :preferences
9
+
10
+ if t.respond_to?(:json)
11
+ t.json :pure_json_data
12
+ t.json :data
13
+ else
14
+ t.text :data
15
+ end
16
+
17
+ if t.respond_to?(:hstore)
18
+ t.hstore :config
19
+ else
20
+ t.text :config
21
+ end
22
+
23
+ if t.respond_to?(:jsonb)
24
+ t.jsonb :pure_jsonb_data
25
+ t.jsonb :settings
26
+ t.jsonb :json_data, null: false, default: {}
27
+ else
28
+ t.text :settings
29
+ t.text :json_data
30
+ end
31
+
32
+ t.datetime :created_at
33
+ t.datetime :updated_at
34
+ end
35
+
36
+ create_table :alarms, force: true do |t|
37
+ t.column :device_id, :integer, null: false
38
+ t.column :alarm_type, :integer, null: false
39
+ t.column :status, :integer, null: false
40
+ t.column :metadata, :text
41
+ t.column :secret_key, :binary
42
+ t.datetime :created_at
43
+ t.datetime :updated_at
44
+ end
45
+
46
+ add_index :alarms, [:device_id, :alarm_type], unique: true, where: 'status <> 0'
47
+ end