datamapper 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. data/CHANGELOG +65 -0
  2. data/README +193 -1
  3. data/do_performance.rb +153 -0
  4. data/environment.rb +45 -0
  5. data/example.rb +119 -22
  6. data/lib/data_mapper.rb +36 -16
  7. data/lib/data_mapper/adapters/abstract_adapter.rb +8 -0
  8. data/lib/data_mapper/adapters/data_object_adapter.rb +360 -0
  9. data/lib/data_mapper/adapters/mysql_adapter.rb +30 -179
  10. data/lib/data_mapper/adapters/postgresql_adapter.rb +90 -199
  11. data/lib/data_mapper/adapters/sql/coersion.rb +32 -3
  12. data/lib/data_mapper/adapters/sql/commands/conditions.rb +97 -128
  13. data/lib/data_mapper/adapters/sql/commands/load_command.rb +234 -231
  14. data/lib/data_mapper/adapters/sql/commands/loader.rb +99 -0
  15. data/lib/data_mapper/adapters/sql/mappings/associations_set.rb +30 -0
  16. data/lib/data_mapper/adapters/sql/mappings/column.rb +68 -6
  17. data/lib/data_mapper/adapters/sql/mappings/schema.rb +6 -3
  18. data/lib/data_mapper/adapters/sql/mappings/table.rb +71 -42
  19. data/lib/data_mapper/adapters/sql/quoting.rb +8 -2
  20. data/lib/data_mapper/adapters/sqlite3_adapter.rb +32 -201
  21. data/lib/data_mapper/associations.rb +21 -7
  22. data/lib/data_mapper/associations/belongs_to_association.rb +96 -80
  23. data/lib/data_mapper/associations/has_and_belongs_to_many_association.rb +158 -67
  24. data/lib/data_mapper/associations/has_many_association.rb +96 -78
  25. data/lib/data_mapper/associations/has_n_association.rb +64 -0
  26. data/lib/data_mapper/associations/has_one_association.rb +49 -79
  27. data/lib/data_mapper/associations/reference.rb +47 -0
  28. data/lib/data_mapper/base.rb +216 -50
  29. data/lib/data_mapper/callbacks.rb +71 -24
  30. data/lib/data_mapper/{session.rb → context.rb} +20 -8
  31. data/lib/data_mapper/database.rb +176 -45
  32. data/lib/data_mapper/embedded_value.rb +65 -0
  33. data/lib/data_mapper/identity_map.rb +12 -4
  34. data/lib/data_mapper/support/active_record_impersonation.rb +12 -8
  35. data/lib/data_mapper/support/enumerable.rb +8 -0
  36. data/lib/data_mapper/support/serialization.rb +13 -0
  37. data/lib/data_mapper/support/string.rb +1 -12
  38. data/lib/data_mapper/support/symbol.rb +3 -0
  39. data/lib/data_mapper/validations/unique_validator.rb +1 -2
  40. data/lib/data_mapper/validations/validation_helper.rb +18 -1
  41. data/performance.rb +109 -34
  42. data/plugins/can_has_sphinx/LICENSE +23 -0
  43. data/plugins/can_has_sphinx/README +4 -0
  44. data/plugins/can_has_sphinx/REVISION +1 -0
  45. data/plugins/can_has_sphinx/Rakefile +22 -0
  46. data/plugins/can_has_sphinx/init.rb +1 -0
  47. data/plugins/can_has_sphinx/install.rb +1 -0
  48. data/plugins/can_has_sphinx/lib/acts_as_sphinx.rb +123 -0
  49. data/plugins/can_has_sphinx/lib/sphinx.rb +460 -0
  50. data/plugins/can_has_sphinx/scripts/sphinx.sh +47 -0
  51. data/plugins/can_has_sphinx/tasks/acts_as_sphinx_tasks.rake +41 -0
  52. data/plugins/dataobjects/REVISION +1 -0
  53. data/plugins/dataobjects/Rakefile +7 -0
  54. data/plugins/dataobjects/do.rb +246 -0
  55. data/plugins/dataobjects/do_mysql.rb +179 -0
  56. data/plugins/dataobjects/do_postgres.rb +181 -0
  57. data/plugins/dataobjects/do_sqlite3.rb +153 -0
  58. data/plugins/dataobjects/spec/do_spec.rb +150 -0
  59. data/plugins/dataobjects/spec/spec_helper.rb +81 -0
  60. data/plugins/dataobjects/swig_mysql/do_mysql.bundle +0 -0
  61. data/plugins/dataobjects/swig_mysql/extconf.rb +33 -0
  62. data/plugins/dataobjects/swig_mysql/mysql_c.c +18800 -0
  63. data/plugins/dataobjects/swig_mysql/mysql_c.i +8 -0
  64. data/plugins/dataobjects/swig_mysql/mysql_supp.i +46 -0
  65. data/plugins/dataobjects/swig_postgres/Makefile +146 -0
  66. data/plugins/dataobjects/swig_postgres/extconf.rb +29 -0
  67. data/plugins/dataobjects/swig_postgres/postgres_c.bundle +0 -0
  68. data/plugins/dataobjects/swig_postgres/postgres_c.c +8185 -0
  69. data/plugins/dataobjects/swig_postgres/postgres_c.i +73 -0
  70. data/plugins/dataobjects/swig_sqlite/db +0 -0
  71. data/plugins/dataobjects/swig_sqlite/extconf.rb +9 -0
  72. data/plugins/dataobjects/swig_sqlite/sqlite3_c.c +4725 -0
  73. data/plugins/dataobjects/swig_sqlite/sqlite_c.i +168 -0
  74. data/rakefile.rb +45 -23
  75. data/spec/acts_as_tree_spec.rb +39 -0
  76. data/spec/associations_spec.rb +220 -0
  77. data/spec/attributes_spec.rb +15 -0
  78. data/spec/base_spec.rb +44 -0
  79. data/spec/callbacks_spec.rb +45 -0
  80. data/spec/can_has_sphinx.rb +6 -0
  81. data/spec/coersion_spec.rb +34 -0
  82. data/spec/conditions_spec.rb +49 -0
  83. data/spec/conversions_to_yaml_spec.rb +17 -0
  84. data/spec/count_command_spec.rb +11 -0
  85. data/spec/delete_command_spec.rb +1 -1
  86. data/spec/embedded_value_spec.rb +23 -0
  87. data/spec/fixtures/animals_exhibits.yaml +2 -0
  88. data/spec/fixtures/people.yaml +18 -1
  89. data/spec/{legacy.rb → legacy_spec.rb} +3 -3
  90. data/spec/load_command_spec.rb +157 -20
  91. data/spec/magic_columns_spec.rb +9 -0
  92. data/spec/mock_adapter.rb +20 -0
  93. data/spec/models/animal.rb +1 -1
  94. data/spec/models/animals_exhibit.rb +6 -0
  95. data/spec/models/exhibit.rb +2 -0
  96. data/spec/models/person.rb +26 -1
  97. data/spec/models/project.rb +19 -0
  98. data/spec/models/sales_person.rb +1 -0
  99. data/spec/models/section.rb +6 -0
  100. data/spec/models/zoo.rb +3 -1
  101. data/spec/query_spec.rb +9 -0
  102. data/spec/save_command_spec.rb +65 -1
  103. data/spec/schema_spec.rb +89 -0
  104. data/spec/single_table_inheritance_spec.rb +27 -0
  105. data/spec/spec_helper.rb +9 -55
  106. data/spec/{symbolic_operators.rb → symbolic_operators_spec.rb} +9 -5
  107. data/spec/{validates_confirmation_of.rb → validates_confirmation_of_spec.rb} +4 -3
  108. data/spec/{validates_format_of.rb → validates_format_of_spec.rb} +5 -4
  109. data/spec/{validates_length_of.rb → validates_length_of_spec.rb} +8 -7
  110. data/spec/{validates_uniqueness_of.rb → validates_uniqueness_of_spec.rb} +7 -10
  111. data/spec/{validations.rb → validations_spec.rb} +24 -6
  112. data/tasks/drivers.rb +20 -0
  113. data/tasks/fixtures.rb +42 -0
  114. metadata +181 -42
  115. data/lib/data_mapper/adapters/sql/commands/advanced_load_command.rb +0 -140
  116. data/lib/data_mapper/adapters/sql/commands/delete_command.rb +0 -113
  117. data/lib/data_mapper/adapters/sql/commands/save_command.rb +0 -141
  118. data/lib/data_mapper/adapters/sql/commands/table_exists_command.rb +0 -33
  119. data/lib/data_mapper/adapters/sql_adapter.rb +0 -163
  120. data/lib/data_mapper/associations/advanced_has_many_association.rb +0 -55
  121. data/lib/data_mapper/support/blank_slate.rb +0 -3
  122. data/lib/data_mapper/support/proc.rb +0 -69
  123. data/lib/data_mapper/support/struct.rb +0 -26
  124. data/lib/data_mapper/unit_of_work.rb +0 -38
  125. data/spec/basic_finder.rb +0 -67
  126. data/spec/belongs_to.rb +0 -47
  127. data/spec/has_and_belongs_to_many.rb +0 -25
  128. data/spec/has_many.rb +0 -34
  129. data/spec/new_record.rb +0 -24
  130. data/spec/sub_select.rb +0 -16
  131. data/spec/support/string_spec.rb +0 -7
@@ -1,66 +1,18 @@
1
- require 'data_mapper/adapters/sql_adapter'
2
- require 'data_mapper/support/connection_pool'
3
-
4
- require 'sqlite3'
1
+ require 'data_mapper/adapters/data_object_adapter'
2
+ begin
3
+ require 'do_sqlite3'
4
+ rescue
5
+ STDERR.puts <<-EOS
6
+ You must install the DataObjects::SQLite3 driver.
7
+ rake dm:install:sqlite3
8
+ EOS
9
+ exit
10
+ end
5
11
 
6
12
  module DataMapper
7
13
  module Adapters
8
14
 
9
- class Sqlite3Adapter < SqlAdapter
10
-
11
- def initialize(configuration)
12
- super
13
- @connections = Support::ConnectionPool.new do
14
- dbh = SQLite3::Database.new(configuration.database)
15
- dbh.results_as_hash = true
16
- dbh
17
- end
18
- end
19
-
20
- def connection
21
- raise ArgumentError.new('Sqlite3Adapter#connection requires a block-parameter') unless block_given?
22
- begin
23
- @connections.hold { |connection| yield connection }
24
- rescue SQLite3::Exception => sle
25
-
26
- @configuration.log.fatal(sle)
27
-
28
- @connections.available_connections.each do |sock|
29
- begin
30
- sock.close
31
- rescue => se
32
- @configuration.log.error(se)
33
- end
34
- end
35
-
36
- @connections.available_connections.clear
37
- raise sle
38
- end
39
- end
40
-
41
- def query(*args)
42
- reader = connection { |db| db.query(escape_sql(*args)) }
43
-
44
- fields = nil
45
- rows = []
46
-
47
- until reader.eof?
48
- hash = reader.next
49
- break if hash.nil?
50
-
51
- fields = hash.keys.select { |field| field.is_a?(String) } unless fields
52
-
53
- rows << fields.map { |field| hash[field] }
54
- end
55
-
56
- reader.close
57
-
58
- struct = Support::Struct::define(fields)
59
-
60
- rows.map do |row|
61
- struct.new(row)
62
- end
63
- end
15
+ class Sqlite3Adapter < DataObjectAdapter
64
16
 
65
17
  TYPES.merge!({
66
18
  :integer => 'INTEGER'.freeze,
@@ -72,155 +24,34 @@ module DataMapper
72
24
  TABLE_QUOTING_CHARACTER = '"'.freeze
73
25
  COLUMN_QUOTING_CHARACTER = '"'.freeze
74
26
 
75
- def type_cast_boolean(value)
76
- case value
77
- when TrueClass, FalseClass then value
78
- when "1", "true", "TRUE" then true
79
- when "0", nil then false
80
- else "Can't type-cast #{value.inspect} to a boolean"
81
- end
82
- end
83
-
84
- def type_cast_datetime(value)
85
- case value
86
- when DateTime then value
87
- when Date then DateTime.new(value)
88
- when String then DateTime::parse(value)
89
- else "Can't type-cast #{value.inspect} to a datetime"
90
- end
27
+ def create_connection
28
+ conn = DataObject::Sqlite3::Connection.new("dbname=#{@configuration.database}")
29
+ conn.open
30
+ return conn
91
31
  end
92
-
93
- module Commands
94
-
95
- class TableExistsCommand
96
- def to_sql
97
- "SELECT name FROM sqlite_master WHERE type = \"table\" AND name = #{table_name}"
98
- end
99
32
 
100
- def call
101
- reader = @adapter.connection { |db| db.query(to_sql) }
102
- result = reader.entries.size > 0
103
- reader.close
104
- result
33
+ module Mappings
34
+ class Table
35
+ def to_exists_sql
36
+ @to_exists_sql || @to_exists_sql = <<-EOS.compress_lines
37
+ SELECT "name"
38
+ FROM "sqlite_master"
39
+ WHERE "type" = "table"
40
+ AND "name" = #{@adapter.quote_value(name)}
41
+ EOS
105
42
  end
106
- end # class TableExistsCommand
107
-
108
- class SaveCommand
109
- def to_create_table_sql
110
- table = @adapter[@instance]
111
-
112
- sql = "CREATE TABLE " << table.to_sql
113
-
114
- sql << " (" << table.columns.map do |column|
115
- column_long_form(column)
116
- end.join(', ') << ")"
117
-
118
- return sql
119
- end
120
-
121
- def column_long_form(column)
122
- long_form = "#{column.to_sql} #{@adapter.class::TYPES[column.type] || column.type}"
123
-
124
- long_form << " NOT NULL" unless column.nullable?
125
- long_form << " PRIMARY KEY" if column.key?
126
- long_form << " default #{column.options[:default]}" if column.options.has_key?(:default)
127
-
128
- return long_form
129
- end
130
-
131
- def execute_insert(sql)
132
- @adapter.connection do |db|
133
- db.query(sql)
134
- db.last_insert_row_id
135
- end
136
- end
137
-
138
- def execute_update(sql)
139
- @adapter.connection do |db|
140
- db.query(sql)
141
- db.total_changes > 0
142
- end
143
- end
144
-
145
- def execute_create_table(sql)
146
- @adapter.connection { |db| db.query(sql) }
147
- true
148
- end
149
- end # class SaveCommand
43
+ end # class Table
150
44
 
151
- class DeleteCommand
152
- def to_truncate_sql
153
- "DELETE FROM " << @adapter[@klass_or_instance].to_sql
154
- end
155
-
156
- def execute(sql)
157
- @adapter.connection do |db|
158
- db.query(sql)
159
- db.total_changes > 0
160
- end
45
+ class Column
46
+ def serial_declaration
47
+ "AUTOINCREMENT"
161
48
  end
162
49
 
163
- def execute_drop(sql)
164
- @adapter.connection { |db| db.query(sql) }
165
- true
50
+ def size
51
+ nil
166
52
  end
167
- end # class DeleteCommand
168
-
169
- class LoadCommand
170
- def eof?(reader)
171
- reader.eof?
172
- end
173
-
174
- def close_reader(reader)
175
- reader.close
176
- end
177
-
178
- def execute(sql)
179
- @adapter.connection { |db| db.query(to_sql) }
180
- end
181
-
182
- def fetch_one(reader)
183
- load(reader.next)
184
- end
185
-
186
- def fetch_all(reader)
187
- fields = nil
188
- rows = []
189
-
190
- until reader.eof?
191
- hash = reader.next
192
- break if hash.nil?
193
-
194
- fields = hash.keys.select { |field| field.is_a?(String) } unless fields
195
-
196
- rows << fields.map { |name| hash[name] }
197
- end
198
-
199
- load_instances(fields, rows)
200
- end
201
-
202
- def fetch_structs(reader)
203
- fields = nil
204
- rows = []
205
-
206
- until reader.eof?
207
- hash = reader.next
208
- break if hash.nil?
209
-
210
- fields = hash.keys.select { |field| field.is_a?(String) } unless fields
211
-
212
- rows << fields.map { |name| hash[name] }
213
- end
214
-
215
- fields = fields.inject({}) do |h,f|
216
- h[f] = fields.index(f); h
217
- end
218
-
219
- load_structs(fields, rows)
220
- end
221
- end
222
-
223
- end # module Commands
53
+ end # class Column
54
+ end # module Mappings
224
55
 
225
56
  end # class Sqlite3Adapter
226
57
 
@@ -1,5 +1,5 @@
1
+ require 'data_mapper/associations/reference'
1
2
  require 'data_mapper/associations/has_many_association'
2
- require 'data_mapper/associations/advanced_has_many_association'
3
3
  require 'data_mapper/associations/belongs_to_association'
4
4
  require 'data_mapper/associations/has_one_association'
5
5
  require 'data_mapper/associations/has_and_belongs_to_many_association'
@@ -8,13 +8,27 @@ module DataMapper
8
8
  module Associations
9
9
 
10
10
  def self.included(base)
11
- base.class_eval do
12
- include DataMapper::Associations::HasMany
13
- include DataMapper::Associations::AdvancedHasMany
14
- include DataMapper::Associations::BelongsTo
15
- include DataMapper::Associations::HasOne
16
- include DataMapper::Associations::HasAndBelongsToMany
11
+ base.extend(ClassMethods)
12
+ end
13
+
14
+ module ClassMethods
15
+
16
+ def has_many(association_name, options = {})
17
+ database.schema[self].associations << HasManyAssociation.new(self, association_name, options)
18
+ end
19
+
20
+ def belongs_to(association_name, options = {})
21
+ database.schema[self].associations << BelongsToAssociation.new(self, association_name, options)
22
+ end
23
+
24
+ def has_and_belongs_to_many(association_name, options = {})
25
+ database.schema[self].associations << HasAndBelongsToManyAssociation.new(self, association_name, options)
26
+ end
27
+
28
+ def has_one(association_name, options = {})
29
+ database.schema[self].associations << HasOneAssociation.new(self, association_name, options)
17
30
  end
31
+
18
32
  end
19
33
 
20
34
  end
@@ -1,110 +1,126 @@
1
+ require 'data_mapper/associations/has_n_association'
2
+
1
3
  module DataMapper
2
4
  module Associations
3
5
 
4
- class BelongsToAssociation
5
-
6
- def initialize(instance, association_name, options)
7
- @instance = instance
8
- @association_name = association_name
9
- @options = options
10
-
11
- @associated_class = if options.has_key?(:class) || options.has_key?(:class_name)
12
- associated_class_name = (options[:class] || options[:class_name])
13
- if associated_class_name.kind_of?(String)
14
- Kernel.const_get(Inflector.classify(associated_class_name))
15
- else
16
- associated_class_name
17
- end
18
- else
19
- Kernel.const_get(Inflector.classify(association_name))
20
- end
21
- end
22
-
23
- def self.setup(klass, association_name, options)
24
- foreign_key = options[:foreign_key] || ("#{association_name}_id")
25
- klass.property foreign_key.to_sym, :integer
6
+ class BelongsToAssociation < HasNAssociation
7
+
8
+ def define_accessor(klass)
9
+ klass.property((@options[:foreign_key] || "#{name}_id").to_sym, :integer)
26
10
 
27
- # Define the association instance method (i.e. Exhibit#zoo)
28
11
  klass.class_eval <<-EOS
29
- def create_#{association_name}(options = {})
30
- #{association_name}_association.create(options)
12
+
13
+ def create_#{@association_name}(options = {})
14
+ #{@association_name}_association.create(options)
31
15
  end
32
16
 
33
- def build_#{association_name}(options = {})
34
- #{association_name}_association.build(options)
17
+ def build_#{@association_name}(options = {})
18
+ #{@association_name}_association.build(options)
35
19
  end
36
20
 
37
- def #{association_name}
38
- # Let the BelongsToAssociation do the finding, just to keep things neat around here...
39
- #{association_name}_association.find
21
+ def #{@association_name}
22
+ #{@association_name}_association.instance
40
23
  end
41
24
 
42
- def #{association_name}=(value)
43
- #{association_name}_association.set(value)
25
+ def #{@association_name}=(value)
26
+ #{@association_name}_association.set(value)
44
27
  end
45
28
 
46
29
  private
47
- def #{association_name}_association
48
- @#{association_name} || (@#{association_name} = BelongsToAssociation.new(self, "#{association_name}", #{options.inspect}))
30
+ def #{@association_name}_association
31
+ @#{@association_name} || (@#{@association_name} = DataMapper::Associations::BelongsToAssociation::Instance.new(self, #{@association_name.inspect}))
49
32
  end
50
33
  EOS
51
-
52
34
  end
53
35
 
54
- def find
55
- return @result unless @result.nil?
56
-
57
- unless @instance.loaded_set.nil?
58
-
59
- # Temp variable for the instance variable name.
60
- setter_method = "#{@association_name}=".to_sym
61
- instance_variable_name = "@#{foreign_key}".to_sym
62
-
63
- set = @instance.loaded_set.group_by { |instance| instance.instance_variable_get(instance_variable_name) }
64
-
65
- # Fetch the foreign objects for all instances in the current object's loaded-set.
66
- @instance.session.all(@associated_class, :id => set.keys).each do |owner|
67
- set[owner.key].each do |instance|
68
- instance.send(setter_method, owner)
69
- end
70
- end
36
+ def foreign_key
37
+ @foreign_key || @foreign_key = begin
38
+ table[@options[:foreign_key] || "#{name}_id".to_sym]
71
39
  end
72
-
73
- return @result
74
40
  end
75
41
 
76
- def create(options = {})
77
- associated = @associated_class.new(options)
78
- if associated.save
79
- @instance.send("#{@associated_class.foreign_key}=", associated.id)
80
- @result = associated
42
+ class Instance < Associations::Reference
43
+
44
+ def instance
45
+ @associated || @associated = begin
46
+ if @instance.loaded_set.nil?
47
+ nil
48
+ else
49
+
50
+ # Temp variable for the instance variable name.
51
+ fk = association.foreign_key.to_sym
52
+
53
+ set = @instance.loaded_set.group_by { |instance| instance.send(fk) }
54
+
55
+ @instance.session.all(association.constant, association.association_table.key.to_sym => set.keys).each do |assoc|
56
+ set[assoc.key].each do |primary_instance|
57
+ primary_instance.send(setter_method, assoc)
58
+ end
59
+ end
60
+
61
+ @associated
62
+ end
63
+ end
81
64
  end
82
- end
83
65
 
84
- def build(options = {})
85
- @result = @associated_class.new(options)
86
- end
66
+ def create(options)
67
+ @associated = association.constant.create(options)
68
+ end
87
69
 
88
- def set(val)
89
- @result = val
90
- end
70
+ def build(options)
71
+ @associated = association.constant.new(options)
72
+ end
91
73
 
92
- def foreign_key
93
- @foreign_key ||= (@options[:foreign_key] || @instance.session.schema[@associated_class].default_foreign_key)
94
- end
74
+ def setter_method
75
+ "#{@association_name}=".to_sym
76
+ end
77
+
78
+ def set(val)
79
+ @instance.instance_variable_set(association.foreign_key.instance_variable_name, val.key)
80
+ @associated = val
81
+ end
95
82
 
96
- end
97
-
98
- module BelongsTo
99
- def self.included(base)
100
- base.extend(ClassMethods)
101
- end
83
+ end # class Instance
102
84
 
103
- module ClassMethods
104
- def belongs_to(association_name, options = {})
105
- BelongsToAssociation.setup(self, association_name, options)
106
- end
107
- end
85
+
86
+ # def find
87
+ # return @result unless @result.nil?
88
+ #
89
+ # unless @instance.loaded_set.nil?
90
+ #
91
+ # # Temp variable for the instance variable name.
92
+ # setter_method = "#{@association_name}=".to_sym
93
+ # instance_variable_name = "@#{foreign_key}".to_sym
94
+ #
95
+ # set = @instance.loaded_set.group_by { |instance| instance.instance_variable_get(instance_variable_name) }
96
+ #
97
+ # # Fetch the foreign objects for all instances in the current object's loaded-set.
98
+ # @instance.session.all(constant, :id => set.keys).each do |owner|
99
+ # set[owner.key].each do |instance|
100
+ # instance.send(setter_method, owner)
101
+ # end
102
+ # end
103
+ # end
104
+ #
105
+ # return @result
106
+ # end
107
+
108
+ # def create(options = {})
109
+ # associated = constant.new(options)
110
+ # if associated.save
111
+ # @instance.send("#{constant.foreign_key}=", associated.id)
112
+ # @result = associated
113
+ # end
114
+ # end
115
+ #
116
+ # def build(options = {})
117
+ # @result = constant.new(options)
118
+ # end
119
+ #
120
+ # def set(val)
121
+ # @result = val
122
+ # end
123
+
108
124
  end
109
125
 
110
126
  end