dbee 1.0.0.pre.alpha.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b130a54954373ada660f1203488dbf1a8c7f086b7d22f6fdc3a1e5f37cbf5266
4
- data.tar.gz: f6d4feef16b9bcf531bddeee8ac7628e95286e3f263c335213daad2cf889bca9
3
+ metadata.gz: dab2bf5eb7197181b4683180b3a3c490621d8e9fb58ee6d0edc99f39be1709ec
4
+ data.tar.gz: e20fc2c2d02819aa2659847c795878ee7c23a1798d5a27b4e220320987016692
5
5
  SHA512:
6
- metadata.gz: 29a0abfebc532e09f81953ec8a9918887bb6ab0bbb3e50d68e1615df71b2c913af0fb9a063ab38381adbcf614df27c6322f297a16d4a90d6481a1608b02ac31b
7
- data.tar.gz: ad4f0531f7bfc44e7d5461f453fecfebccb3afbbfb997d111a154030188d9ce4a4d206fdcb9d466027439a182e7c4fb3127327b104223e615be203bd9775b857
6
+ metadata.gz: c55baaace5f9e37d4d71a2465780ef60b20d06956ab27b45d9d91bc841757f4d393b05cfc757aead9f6acd4225b3b06a55b5521b78f6686e42cc4391fc9f72db
7
+ data.tar.gz: 96bcb8c21d3974b4d46608e3e80658bd6608dc588d781da4676ea335bc8dc75ffc7f6dd202e691bbc905589f3f81e8a2b00e9f770a1d21a58de1729382f0b3e2
@@ -14,7 +14,7 @@ Metrics/MethodLength:
14
14
  Max: 25
15
15
 
16
16
  AllCops:
17
- TargetRubyVersion: 2.5
17
+ TargetRubyVersion: 2.4
18
18
 
19
19
  Metrics/AbcSize:
20
20
  Max: 16
@@ -4,8 +4,8 @@ env:
4
4
  language: ruby
5
5
  rvm:
6
6
  # Build on the latest stable of all supported Rubies (https://www.ruby-lang.org/en/downloads/):
7
- - 2.5.3
8
- - 2.6.0
7
+ - 2.4.6
8
+ - 2.5.5
9
9
  - 2.6.3
10
10
  cache: bundler
11
11
  before_script:
@@ -1,3 +1,7 @@
1
+ # 1.0.0 (August 22nd, 2019)
2
+
3
+ Initial release.
4
+
1
5
  # 1.0.0-alpha (August 18th, 2019)
2
6
 
3
7
  Added initial implementation.
data/README.md CHANGED
@@ -11,7 +11,7 @@ Dbee arose out of a need for an ad-hoc reporting solution that included:
11
11
 
12
12
  Dbee provides a very simple Data Modeling and Query API's and as such it is not meant to replace a traditional ORM or your data persistence layer, but compliment them. This library's goal is to output the SQL statement needed and **nothing** more.
13
13
 
14
- Other solutions considered included:
14
+ Other solutions considered:
15
15
 
16
16
  * [Squeel](https://github.com/activerecord-hackery/squeel) - Was in production use up until Rails 5, then saw compatibility issues.
17
17
  * [BabySqueel](https://github.com/rzane/baby_squeel) - Tested with some success up until Rails 5.2.1, then saw compatibility issues.
@@ -82,7 +82,7 @@ There are two ways to model this schema using Dbee:
82
82
 
83
83
  #### Code-First Data Modeling
84
84
 
85
- Code-first data modeling involves creating sub-classes of Dbee::Base that describes the tables, columns, and associations. We could model the above example as:
85
+ Code-first data modeling involves creating sub-classes of Dbee::Base that describes the tables and associations. We could model the above example as:
86
86
 
87
87
  ````ruby
88
88
  module ReadmeDataModels
@@ -115,8 +115,6 @@ module ReadmeDataModels
115
115
  end
116
116
 
117
117
  class Practices < Dbee::Base
118
- boolean_column :active, nullable: false
119
-
120
118
  association :patients, model: Patients, constraints: {
121
119
  type: :reference, name: :practice_id, parent: :id
122
120
  }
@@ -125,10 +123,8 @@ end
125
123
 
126
124
  ````
127
125
 
128
- A couple notes:
126
+ **Note:** the 'table' directive is optional, and if omitted, the classes name will be turned into snake_case and used. In the above example you can see we wanted the class name of PhoneNumbers but the table is actually 'phones'
129
127
 
130
- * the 'table' directive is optional, and if omitted, the classes name will be turned into snake_case and used. In the above example you can see we wanted the class name of PhoneNumbers but the table is actually 'phones'
131
- * it is not required that all columns be explicitly defined but it does provide value coercion. By default, all undefined columns are of type Dbee::Model::Columns::Undefined.
132
128
 
133
129
  #### Configuration-First Data Modeling
134
130
 
@@ -136,10 +132,6 @@ You can choose to alternatively describe your data model using configuration. T
136
132
 
137
133
  ````yaml
138
134
  name: practices
139
- columns:
140
- - name: active
141
- type: boolean
142
- nullable: false
143
135
  models:
144
136
  - name: patients
145
137
  constraints:
@@ -19,7 +19,7 @@ Gem::Specification.new do |s|
19
19
  s.homepage = 'https://github.com/bluemarblepayroll/dbee'
20
20
  s.license = 'MIT'
21
21
 
22
- s.required_ruby_version = '>= 2.5.3'
22
+ s.required_ruby_version = '>= 2.4.6'
23
23
 
24
24
  s.add_dependency('acts_as_hashable', '~>1', '>=1.1.0')
25
25
 
@@ -27,7 +27,7 @@ Gem::Specification.new do |s|
27
27
  s.add_development_dependency('pry', '~>0')
28
28
  s.add_development_dependency('rake', '~> 12')
29
29
  s.add_development_dependency('rspec')
30
- s.add_development_dependency('rubocop', '~>0.63.1')
31
- s.add_development_dependency('simplecov', '~>0.16.1')
32
- s.add_development_dependency('simplecov-console', '~>0.4.2')
30
+ s.add_development_dependency('rubocop', '~>0.74.0')
31
+ s.add_development_dependency('simplecov', '~>0.17.0')
32
+ s.add_development_dependency('simplecov-console', '~>0.5.0')
33
33
  end
@@ -12,10 +12,6 @@ module Dbee
12
12
  # Model declaration.
13
13
  class Base
14
14
  class << self
15
- def boolean_column(name, opts = {})
16
- columns_by_name[name.to_s] = opts.merge(name: name, type: :boolean)
17
- end
18
-
19
15
  def table(name)
20
16
  @table_name = name.to_s
21
17
 
@@ -39,10 +35,6 @@ module Dbee
39
35
  @table_name || ''
40
36
  end
41
37
 
42
- def columns_by_name
43
- @columns_by_name ||= {}
44
- end
45
-
46
38
  def associations_by_name
47
39
  @associations_by_name ||= {}
48
40
  end
@@ -55,12 +47,6 @@ module Dbee
55
47
  subclasses.find(&:table_name?)&.table_name || ''
56
48
  end
57
49
 
58
- def inherited_columns_by_name
59
- reversed_subclasses.each_with_object({}) do |subclass, memo|
60
- memo.merge!(subclass.columns_by_name)
61
- end
62
- end
63
-
64
50
  def inherited_associations_by_name
65
51
  reversed_subclasses.each_with_object({}) do |subclass, memo|
66
52
  memo.merge!(subclass.associations_by_name)
@@ -79,7 +65,6 @@ module Dbee
79
65
 
80
66
  def model_config(name, constraints)
81
67
  {
82
- columns: columns,
83
68
  constraints: constraints,
84
69
  models: associations,
85
70
  name: name,
@@ -97,12 +82,6 @@ module Dbee
97
82
  inherited_table.empty? ? inflected_name : inherited_table
98
83
  end
99
84
 
100
- def columns
101
- inherited_columns_by_name.values.each_with_object([]) do |config, memo|
102
- memo << Model::Columns.make(config)
103
- end
104
- end
105
-
106
85
  def associations
107
86
  inherited_associations_by_name.values.each_with_object([]) do |config, memo|
108
87
  model_klass = config[:model]
@@ -7,7 +7,6 @@
7
7
  # LICENSE file in the root directory of this source tree.
8
8
  #
9
9
 
10
- require_relative 'model/columns'
11
10
  require_relative 'model/constraints'
12
11
 
13
12
  module Dbee
@@ -20,18 +19,17 @@ module Dbee
20
19
 
21
20
  private_constant :JOIN_CHAR
22
21
 
23
- class ModelNotFound < StandardError; end
22
+ class ModelNotFoundError < StandardError; end
24
23
 
25
24
  attr_reader :constraints, :name
26
25
 
27
- def initialize(name:, columns: [], constraints: [], models: [], table: '')
26
+ def initialize(name:, constraints: [], models: [], table: '')
28
27
  raise ArgumentError, 'name is required' if name.to_s.empty?
29
28
 
30
- @name = name.to_s
31
- @columns_by_name = name_hash(Columns.array(columns))
32
- @constraints = Constraints.array(constraints)
33
- @models_by_name = name_hash(Model.array(models))
34
- @table = table.to_s
29
+ @name = name.to_s
30
+ @constraints = Constraints.array(constraints)
31
+ @models_by_name = name_hash(Model.array(models))
32
+ @table = table.to_s
35
33
 
36
34
  freeze
37
35
  end
@@ -48,14 +46,6 @@ module Dbee
48
46
  models_by_name.values
49
47
  end
50
48
 
51
- def columns
52
- columns_by_name.values
53
- end
54
-
55
- def column(name)
56
- columns_by_name[name.to_s] || Columns::Undefined.new(name: name)
57
- end
58
-
59
49
  def ancestors(parts = [], alias_chain = [], found = {})
60
50
  return found if Array(parts).empty?
61
51
 
@@ -65,7 +55,7 @@ module Dbee
65
55
 
66
56
  model = models_by_name[model_name.to_s]
67
57
 
68
- raise ModelNotFound, "Cannot traverse: #{model_name}" unless model
58
+ raise ModelNotFoundError, "Cannot traverse: #{model_name}" unless model
69
59
 
70
60
  new_alias_chain = alias_chain + [model_name]
71
61
 
@@ -80,13 +70,12 @@ module Dbee
80
70
  other.name == name &&
81
71
  other.table == table &&
82
72
  other.models == models &&
83
- other.constraints == constraints &&
84
- other.columns == columns
73
+ other.constraints == constraints
85
74
  end
86
75
  alias eql? ==
87
76
 
88
77
  private
89
78
 
90
- attr_reader :models_by_name, :columns_by_name
79
+ attr_reader :models_by_name
91
80
  end
92
81
  end
@@ -8,5 +8,5 @@
8
8
  #
9
9
 
10
10
  module Dbee
11
- VERSION = '1.0.0-alpha.3'
11
+ VERSION = '1.0.0'
12
12
  end
@@ -55,28 +55,6 @@ describe Dbee::Model do
55
55
  end
56
56
  end
57
57
 
58
- describe '#column' do
59
- let(:yaml_entities) { yaml_fixture('models.yaml') }
60
-
61
- let(:entity_hash) { yaml_entities['Theaters, Members, and Movies'] }
62
-
63
- subject { described_class.make(entity_hash) }
64
-
65
- specify 'returns column instance if it exists' do
66
- expected = Dbee::Model::Columns::Boolean.make(name: 'active', nullable: false)
67
- expect(subject.column('active')).to eq(expected)
68
-
69
- expected = Dbee::Model::Columns::Boolean.make(name: 'inspected', nullable: true)
70
- expect(subject.column('inspected')).to eq(expected)
71
- end
72
-
73
- specify 'returns unknown column instance if it does not exist' do
74
- expected = Dbee::Model::Columns::Boolean.make(name: 'doesnt_exist')
75
-
76
- expect(subject.column('doesnt_exist')).to eq(expected)
77
- end
78
- end
79
-
80
58
  describe '#ancestors' do
81
59
  let(:yaml_entities) { yaml_fixture('models.yaml') }
82
60
 
@@ -49,12 +49,9 @@ module Models
49
49
  end
50
50
 
51
51
  class TheatersBase < Dbee::Base
52
- boolean_column :active, nullable: false
53
52
  end
54
53
 
55
54
  class Theaters < TheatersBase
56
- boolean_column :inspected
57
-
58
55
  association :members, model: Members, constraints: [
59
56
  { type: :reference, name: :tid, parent: :id },
60
57
  { type: :reference, name: :partition, parent: :partition }
@@ -111,8 +108,6 @@ module ReadmeDataModels
111
108
  end
112
109
 
113
110
  class Practices < Dbee::Base
114
- boolean_column :active, nullable: false
115
-
116
111
  association :patients, model: Patients, constraints: {
117
112
  type: :reference, name: :practice_id, parent: :id
118
113
  }
@@ -1,12 +1,6 @@
1
1
  Theaters, Members, and Movies:
2
2
  name: theaters
3
3
  table: theaters
4
- columns:
5
- - name: active
6
- type: boolean
7
- nullable: false
8
- - name: inspected
9
- type: boolean # this one is nullable
10
4
  models:
11
5
  - name: members
12
6
  table: members
@@ -66,10 +60,6 @@ Theaters, Members, and Movies:
66
60
  value: comedy
67
61
  Readme:
68
62
  name: practices
69
- columns:
70
- - name: active
71
- type: boolean
72
- nullable: false
73
63
  models:
74
64
  - name: patients
75
65
  constraints:
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbee
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre.alpha.3
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Ruggio
@@ -92,42 +92,42 @@ dependencies:
92
92
  requirements:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
- version: 0.63.1
95
+ version: 0.74.0
96
96
  type: :development
97
97
  prerelease: false
98
98
  version_requirements: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - "~>"
101
101
  - !ruby/object:Gem::Version
102
- version: 0.63.1
102
+ version: 0.74.0
103
103
  - !ruby/object:Gem::Dependency
104
104
  name: simplecov
105
105
  requirement: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
- version: 0.16.1
109
+ version: 0.17.0
110
110
  type: :development
111
111
  prerelease: false
112
112
  version_requirements: !ruby/object:Gem::Requirement
113
113
  requirements:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
- version: 0.16.1
116
+ version: 0.17.0
117
117
  - !ruby/object:Gem::Dependency
118
118
  name: simplecov-console
119
119
  requirement: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - "~>"
122
122
  - !ruby/object:Gem::Version
123
- version: 0.4.2
123
+ version: 0.5.0
124
124
  type: :development
125
125
  prerelease: false
126
126
  version_requirements: !ruby/object:Gem::Requirement
127
127
  requirements:
128
128
  - - "~>"
129
129
  - !ruby/object:Gem::Version
130
- version: 0.4.2
130
+ version: 0.5.0
131
131
  description: " Dbee provides a simple-to-use data modeling and query API. The
132
132
  query API can produce SQL using other ORMs, such as Arel/ActiveRecord. The targeted
133
133
  use-case for Dbee is ad-hoc reporting, so the total SQL feature-set that Dbee supports
@@ -156,9 +156,6 @@ files:
156
156
  - lib/dbee.rb
157
157
  - lib/dbee/base.rb
158
158
  - lib/dbee/model.rb
159
- - lib/dbee/model/columns.rb
160
- - lib/dbee/model/columns/boolean.rb
161
- - lib/dbee/model/columns/undefined.rb
162
159
  - lib/dbee/model/constraints.rb
163
160
  - lib/dbee/model/constraints/base.rb
164
161
  - lib/dbee/model/constraints/reference.rb
@@ -183,8 +180,6 @@ files:
183
180
  - lib/dbee/query/sorter.rb
184
181
  - lib/dbee/version.rb
185
182
  - spec/dbee/base_spec.rb
186
- - spec/dbee/model/columns/boolean_spec.rb
187
- - spec/dbee/model/columns/undefined_spec.rb
188
183
  - spec/dbee/model/constraints/base_spec.rb
189
184
  - spec/dbee/model/constraints/reference_spec.rb
190
185
  - spec/dbee/model/constraints/static_spec.rb
@@ -213,12 +208,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
213
208
  requirements:
214
209
  - - ">="
215
210
  - !ruby/object:Gem::Version
216
- version: 2.5.3
211
+ version: 2.4.6
217
212
  required_rubygems_version: !ruby/object:Gem::Requirement
218
213
  requirements:
219
- - - ">"
214
+ - - ">="
220
215
  - !ruby/object:Gem::Version
221
- version: 1.3.1
216
+ version: '0'
222
217
  requirements: []
223
218
  rubygems_version: 3.0.3
224
219
  signing_key:
@@ -226,8 +221,6 @@ specification_version: 4
226
221
  summary: Adhoc Reporting SQL Generator
227
222
  test_files:
228
223
  - spec/dbee/base_spec.rb
229
- - spec/dbee/model/columns/boolean_spec.rb
230
- - spec/dbee/model/columns/undefined_spec.rb
231
224
  - spec/dbee/model/constraints/base_spec.rb
232
225
  - spec/dbee/model/constraints/reference_spec.rb
233
226
  - spec/dbee/model/constraints/static_spec.rb
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
- #
6
- # This source code is licensed under the MIT license found in the
7
- # LICENSE file in the root directory of this source tree.
8
- #
9
-
10
- require_relative 'columns/boolean'
11
- require_relative 'columns/undefined'
12
-
13
- module Dbee
14
- class Model
15
- # Top-level class that allows for the making of columns. For example, you can call this as:
16
- # - Columns.make(type: :boolean, name: :something)
17
- # - Columns.make(type: :undefined, name: :something_else)
18
- class Columns
19
- acts_as_hashable_factory
20
-
21
- register 'boolean', Boolean
22
- register 'undefined', Undefined
23
- end
24
- end
25
- end
@@ -1,61 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
- #
6
- # This source code is licensed under the MIT license found in the
7
- # LICENSE file in the root directory of this source tree.
8
- #
9
-
10
- require_relative 'undefined'
11
-
12
- module Dbee
13
- class Model
14
- class Columns
15
- # Describes a boolean column that can accept a wide range of values and still resolves to
16
- # a boolean, such as: true, t, yes, y, 1, false, f, n, 0, nil, null.
17
- class Boolean < Undefined
18
- attr_reader :nullable
19
-
20
- alias nullable? nullable
21
-
22
- def initialize(name:, nullable: true)
23
- super(name: name)
24
-
25
- @nullable = nullable
26
-
27
- freeze
28
- end
29
-
30
- def ==(other)
31
- super && other.nullable == nullable
32
- end
33
- alias eql? ==
34
-
35
- def coerce(value)
36
- if nullable? && nully?(value)
37
- nil
38
- elsif truthy?(value)
39
- true
40
- else
41
- false
42
- end
43
- end
44
-
45
- private
46
-
47
- def null_or_empty?(val)
48
- val.nil? || val.to_s.empty?
49
- end
50
-
51
- def nully?(val)
52
- null_or_empty?(val) || val.to_s.match?(/\A(nil|null)$\z/i)
53
- end
54
-
55
- def truthy?(val)
56
- val.to_s.match?(/\A(true|t|yes|y|1)$\z/i)
57
- end
58
- end
59
- end
60
- end
61
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
- #
6
- # This source code is licensed under the MIT license found in the
7
- # LICENSE file in the root directory of this source tree.
8
- #
9
-
10
- module Dbee
11
- class Model
12
- class Columns
13
- # Any non-configured column will automatically be this type.
14
- # Also doubles as the base class for all columns specification subclasses.
15
- class Undefined
16
- acts_as_hashable
17
-
18
- attr_reader :name
19
-
20
- def initialize(name:)
21
- raise ArgumentError, 'name is required' if name.to_s.empty?
22
-
23
- @name = name.to_s
24
- end
25
-
26
- def coerce(value)
27
- value
28
- end
29
-
30
- def ==(other)
31
- other.name == name
32
- end
33
- alias eql? ==
34
- end
35
- end
36
- end
37
- end
@@ -1,74 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
- #
6
- # This source code is licensed under the MIT license found in the
7
- # LICENSE file in the root directory of this source tree.
8
- #
9
-
10
- # frozen_string_literal: true
11
-
12
- #
13
- # Copyright (c) 2019-present, Blue Marble Payroll, LLC
14
- #
15
- # This source code is licensed under the MIT license found in the
16
- # LICENSE file in the root directory of this source tree.
17
- #
18
-
19
- require 'spec_helper'
20
-
21
- describe Dbee::Model::Columns::Boolean do
22
- specify 'equality compares attributes' do
23
- config = {
24
- name: :a,
25
- nullable: false
26
- }
27
-
28
- column1 = described_class.make(config)
29
- column2 = described_class.make(config)
30
-
31
- expect(column1).to eq(column2)
32
- expect(column1).to eql(column2)
33
- end
34
-
35
- describe '#coerce' do
36
- context 'when not nullable and value is a string' do
37
- subject { described_class.make(name: :active, nullable: false) }
38
-
39
- %w[y Y yes YES Yes yEs t True TRUE T TrUe 1].each do |value|
40
- it "converts #{value} to true" do
41
- expect(subject.coerce(value)).to be true
42
- end
43
- end
44
-
45
- %w[n N no NO No nO f F FALSE 0 nil null Nil Null NULL NIL].each do |value|
46
- it "converts #{value} to true" do
47
- expect(subject.coerce(value)).to be false
48
- end
49
- end
50
- end
51
-
52
- context 'when nullable and value is a string' do
53
- subject { described_class.make(name: :active, nullable: true) }
54
-
55
- %w[y Y yes YES Yes yEs t True TRUE T TrUe 1].each do |value|
56
- it "converts #{value} to true" do
57
- expect(subject.coerce(value)).to be true
58
- end
59
- end
60
-
61
- %w[n N no NO No nO f F FALSE 0].each do |value|
62
- it "converts #{value} to true" do
63
- expect(subject.coerce(value)).to be false
64
- end
65
- end
66
-
67
- %w[nil null Nil Null NULL NIL].each do |value|
68
- it "converts #{value} to true" do
69
- expect(subject.coerce(value)).to be nil
70
- end
71
- end
72
- end
73
- end
74
- end
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
- #
6
- # This source code is licensed under the MIT license found in the
7
- # LICENSE file in the root directory of this source tree.
8
- #
9
-
10
- # frozen_string_literal: true
11
-
12
- #
13
- # Copyright (c) 2019-present, Blue Marble Payroll, LLC
14
- #
15
- # This source code is licensed under the MIT license found in the
16
- # LICENSE file in the root directory of this source tree.
17
- #
18
-
19
- require 'spec_helper'
20
-
21
- describe Dbee::Model::Columns::Undefined do
22
- let(:config) { { name: :some_column } }
23
-
24
- subject { described_class.make(config) }
25
-
26
- specify 'equality compares attributes' do
27
- column1 = described_class.make(config)
28
- column2 = described_class.make(config)
29
-
30
- expect(column1).to eq(column2)
31
- expect(column1).to eql(column2)
32
- end
33
-
34
- specify '#coerce returns value' do
35
- value = 'abc123'
36
- expect(subject.coerce(value)).to eq(value)
37
- end
38
- end