activerecord-postgresql-extensions 0.0.7

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 (34) hide show
  1. data/MIT-LICENSE +23 -0
  2. data/README.rdoc +32 -0
  3. data/Rakefile +42 -0
  4. data/VERSION +1 -0
  5. data/lib/activerecord-postgresql-extensions.rb +30 -0
  6. data/lib/postgresql_extensions/foreign_key_associations.rb +367 -0
  7. data/lib/postgresql_extensions/postgresql_adapter_extensions.rb +646 -0
  8. data/lib/postgresql_extensions/postgresql_constraints.rb +579 -0
  9. data/lib/postgresql_extensions/postgresql_functions.rb +345 -0
  10. data/lib/postgresql_extensions/postgresql_geometry.rb +212 -0
  11. data/lib/postgresql_extensions/postgresql_indexes.rb +219 -0
  12. data/lib/postgresql_extensions/postgresql_languages.rb +80 -0
  13. data/lib/postgresql_extensions/postgresql_permissions.rb +322 -0
  14. data/lib/postgresql_extensions/postgresql_rules.rb +112 -0
  15. data/lib/postgresql_extensions/postgresql_schemas.rb +49 -0
  16. data/lib/postgresql_extensions/postgresql_sequences.rb +222 -0
  17. data/lib/postgresql_extensions/postgresql_tables.rb +308 -0
  18. data/lib/postgresql_extensions/postgresql_triggers.rb +131 -0
  19. data/lib/postgresql_extensions/postgresql_types.rb +17 -0
  20. data/lib/postgresql_extensions/postgresql_views.rb +103 -0
  21. data/postgresql-extensions.gemspec +50 -0
  22. data/test/adapter_test.rb +45 -0
  23. data/test/constraints_test.rb +98 -0
  24. data/test/functions_test.rb +112 -0
  25. data/test/geometry_test.rb +43 -0
  26. data/test/index_test.rb +68 -0
  27. data/test/languages_test.rb +48 -0
  28. data/test/permissions_test.rb +163 -0
  29. data/test/rules_test.rb +32 -0
  30. data/test/schemas_test.rb +43 -0
  31. data/test/sequences_test.rb +90 -0
  32. data/test/tables_test.rb +49 -0
  33. data/test/test_helper.rb +64 -0
  34. metadata +97 -0
@@ -0,0 +1,112 @@
1
+
2
+ $: << File.dirname(__FILE__)
3
+ require 'test_helper'
4
+
5
+ class FunctionsTests < Test::Unit::TestCase
6
+ include PostgreSQLExtensionsTestHelper
7
+
8
+ def test_create_function
9
+ Mig.create_function(:test, :integer, :integer, :sql) do
10
+ "select 10;"
11
+ end
12
+
13
+ Mig.create_function(:test, :integer, :integer, :sql, {
14
+ :force => true,
15
+ :delimiter => '$__$',
16
+ :behavior => :immutable,
17
+ :on_null_input => :strict,
18
+ :cost => 1,
19
+ :rows => 10,
20
+ :set => {
21
+ 'TIME ZONE' => 'America/Halifax'
22
+ }
23
+ }) do
24
+ "return 10;"
25
+ end
26
+
27
+ assert_equal([
28
+ %{CREATE FUNCTION "test"(integer) RETURNS integer AS $$
29
+ select 10;
30
+ $$
31
+ LANGUAGE "sql"},
32
+
33
+ %{CREATE OR REPLACE FUNCTION "test"(integer) RETURNS integer AS $__$
34
+ return 10;
35
+ $__$
36
+ LANGUAGE "sql"
37
+ IMMUTABLE
38
+ STRICT
39
+ COST 1
40
+ ROWS 10
41
+ SET TIME ZONE "America/Halifax"}
42
+ ], statements)
43
+ end
44
+
45
+ def test_drop_function
46
+ Mig.drop_function(:test, :integer)
47
+ Mig.drop_function(:test, :integer, :if_exists => true, :cascade => true)
48
+
49
+ assert_equal([
50
+ "DROP FUNCTION \"test\"(integer)",
51
+ "DROP FUNCTION IF EXISTS \"test\"(integer) CASCADE"
52
+ ], statements)
53
+ end
54
+
55
+ def test_rename_function
56
+ Mig.rename_function(:test, 'integer, text', :foo)
57
+
58
+ assert_equal([
59
+ "ALTER FUNCTION \"test\"(integer, text) RENAME TO \"foo\""
60
+ ], statements)
61
+ end
62
+
63
+ def test_alter_function_owner
64
+ Mig.alter_function_owner(:test, 'integer, text', :admin)
65
+
66
+ assert_equal([
67
+ "ALTER FUNCTION \"test\"(integer, text) OWNER TO \"admin\""
68
+ ], statements)
69
+ end
70
+
71
+ def test_alter_function_schema
72
+ Mig.alter_function_schema(:test, 'integer, text', :geospatial)
73
+
74
+ assert_equal([
75
+ "ALTER FUNCTION \"test\"(integer, text) SET SCHEMA \"geospatial\""
76
+ ], statements)
77
+ end
78
+
79
+ def test_alter_function
80
+ Mig.alter_function('my_function', 'integer', :rename_to => 'another_function')
81
+ Mig.alter_function('another_function', 'integer', :owner_to => 'jdoe')
82
+ Mig.alter_function('my_function', 'integer') do |f|
83
+ f.rename_to 'another_function'
84
+ f.owner_to 'jdoe'
85
+ f.set_schema 'foo'
86
+ f.behavior 'immutable'
87
+ f.security 'invoker'
88
+ f.cost 10
89
+ f.rows 10
90
+ f.set({
91
+ :log_duration => 0.4
92
+ })
93
+ f.reset :all
94
+ f.reset %w{ debug_assertions trace_notify }
95
+ end
96
+
97
+ assert_equal([
98
+ %{ALTER FUNCTION "my_function"(integer) RENAME TO "another_function"},
99
+ %{ALTER FUNCTION "another_function"(integer) OWNER TO "jdoe"},
100
+ %{ALTER FUNCTION "my_function"(integer) RENAME TO "another_function";
101
+ ALTER FUNCTION "another_function"(integer) OWNER TO "jdoe";
102
+ ALTER FUNCTION "another_function"(integer) SET SCHEMA "foo";
103
+ ALTER FUNCTION "another_function"(integer) IMMUTABLE;
104
+ ALTER FUNCTION "another_function"(integer) SECURITY INVOKER;
105
+ ALTER FUNCTION "another_function"(integer) COST 10;
106
+ ALTER FUNCTION "another_function"(integer) ROWS 10;
107
+ ALTER FUNCTION "another_function"(integer) SET "log_duration" TO "0.4";
108
+ ALTER FUNCTION "another_function"(integer) RESET ALL;
109
+ ALTER FUNCTION "another_function"(integer) RESET "debug_assertions" RESET "trace_notify"}
110
+ ], statements)
111
+ end
112
+ end
@@ -0,0 +1,43 @@
1
+
2
+ $: << File.dirname(__FILE__)
3
+ require 'test_helper'
4
+
5
+ class GeometryTests < Test::Unit::TestCase
6
+ include PostgreSQLExtensionsTestHelper
7
+
8
+ def test_create_geometry
9
+ Mig.create_table(:foo) do |t|
10
+ t.geometry :the_geom, :srid => 4326
11
+ end
12
+
13
+ assert_equal([
14
+ %{CREATE TABLE "foo" (
15
+ "id" serial primary key,
16
+ "the_geom" geometry,
17
+ CONSTRAINT "enforce_srid_the_geom" CHECK (srid("the_geom") = (4326)),
18
+ CONSTRAINT "enforce_dims_the_geom" CHECK (ndims("the_geom") = 2)
19
+ )},
20
+ %{DELETE FROM "geometry_columns" WHERE f_table_catalog = '' AND f_table_schema = 'public' AND f_table_name = 'foo' AND f_geometry_column = 'the_geom'},
21
+ %{INSERT INTO "geometry_columns" VALUES ('', 'public', 'foo', 'the_geom', 2, 4326, 'geometry')},
22
+ %{CREATE INDEX "foo_the_geom_gist_index" ON "foo" USING "gist"("the_geom")}
23
+ ], statements)
24
+ end
25
+
26
+ def test_create_geometry_with_schema
27
+ Mig.create_table('public.foo') do |t|
28
+ t.geometry :the_geom, :srid => 4326
29
+ end
30
+
31
+ assert_equal([
32
+ %{CREATE TABLE "public"."foo" (
33
+ "id" serial primary key,
34
+ "the_geom" geometry,
35
+ CONSTRAINT "enforce_srid_the_geom" CHECK (srid("the_geom") = (4326)),
36
+ CONSTRAINT "enforce_dims_the_geom" CHECK (ndims("the_geom") = 2)
37
+ )},
38
+ %{DELETE FROM "geometry_columns" WHERE f_table_catalog = '' AND f_table_schema = 'public' AND f_table_name = 'foo' AND f_geometry_column = 'the_geom'},
39
+ %{INSERT INTO "geometry_columns" VALUES ('', 'public', 'foo', 'the_geom', 2, 4326, 'geometry')},
40
+ %{CREATE INDEX "foo_the_geom_gist_index" ON "foo" USING "gist"("the_geom")}
41
+ ], statements)
42
+ end
43
+ end
@@ -0,0 +1,68 @@
1
+
2
+ $: << File.dirname(__FILE__)
3
+ require 'test_helper'
4
+
5
+ class IndexTests < Test::Unit::TestCase
6
+ include PostgreSQLExtensionsTestHelper
7
+
8
+ def test_create_index
9
+ Mig.create_index(:foo_names_idx, :foo, [ :first_name, :last_name ])
10
+ Mig.create_index(:foo_bar_id_idx, :foo, :column => :bar_id)
11
+ Mig.create_index(:foo_coalesce_bar_id_idx, :foo, :expression => 'COALESCE(bar_id, 0)')
12
+ Mig.create_index(:foo_search_idx, :foo, :search, :using => :gin)
13
+
14
+ Mig.create_index(:foo_names_idx, :foo, {
15
+ :column => :name,
16
+ :opclass => 'text_pattern_ops'
17
+ })
18
+
19
+ Mig.create_index(:foo_bar_id_idx, :foo, {
20
+ :column => :bar_id,
21
+ :order => :asc,
22
+ :nulls => :last
23
+ }, {
24
+ :fill_factor => 10,
25
+ :unique => true,
26
+ :concurrently => true,
27
+ :tablespace => 'fubar',
28
+ :conditions => 'bar_id IS NOT NULL'
29
+ })
30
+
31
+ assert_equal([
32
+ "CREATE INDEX \"foo_names_idx\" ON \"foo\"(\"first_name\", \"last_name\")",
33
+ "CREATE INDEX \"foo_bar_id_idx\" ON \"foo\"(\"bar_id\")",
34
+ "CREATE INDEX \"foo_coalesce_bar_id_idx\" ON \"foo\"((COALESCE(bar_id, 0)))",
35
+ "CREATE INDEX \"foo_search_idx\" ON \"foo\" USING \"gin\"(\"search\")",
36
+ "CREATE INDEX \"foo_names_idx\" ON \"foo\"(\"name\" \"text_pattern_ops\")",
37
+ "CREATE UNIQUE INDEX CONCURRENTLY \"foo_bar_id_idx\" ON \"foo\"(\"bar_id\" ASC NULLS LAST) WITH (FILLFACTOR = 10) TABLESPACE \"fubar\" WHERE bar_id IS NOT NULL"
38
+ ], statements)
39
+ end
40
+
41
+ def test_drop_index
42
+ Mig.drop_index(:foo_names_idx)
43
+ Mig.drop_index(:foo_names_idx, :if_exists => true)
44
+ Mig.drop_index(:foo_names_idx, :cascade => true)
45
+
46
+ assert_equal([
47
+ "DROP INDEX \"foo_names_idx\"",
48
+ "DROP INDEX IF EXISTS \"foo_names_idx\"",
49
+ "DROP INDEX \"foo_names_idx\" CASCADE"
50
+ ], statements)
51
+ end
52
+
53
+ def test_rename_index
54
+ Mig.rename_index(:foo_names_idx, :foo_renamed_idx)
55
+
56
+ assert_equal([
57
+ "ALTER INDEX \"foo_names_idx\" RENAME TO \"foo_renamed_idx\""
58
+ ], statements)
59
+ end
60
+
61
+ def test_alter_index_tablespace
62
+ Mig.alter_index_tablespace(:foo_names_idx, :fubar)
63
+
64
+ assert_equal([
65
+ "ALTER INDEX \"foo_names_idx\" SET TABLESPACE \"fubar\""
66
+ ], statements)
67
+ end
68
+ end
@@ -0,0 +1,48 @@
1
+
2
+ $: << File.dirname(__FILE__)
3
+ require 'test_helper'
4
+
5
+ class LanguagesTests < Test::Unit::TestCase
6
+ include PostgreSQLExtensionsTestHelper
7
+
8
+ def test_create_language
9
+ Mig.create_language(:foo)
10
+ Mig.create_language(
11
+ :foo,
12
+ :trusted => true,
13
+ :call_handler => 'plpgsql',
14
+ :validator => 'test()'
15
+ )
16
+
17
+ assert_equal([
18
+ "CREATE PROCEDURAL LANGUAGE \"foo\"",
19
+ "CREATE TRUSTED PROCEDURAL LANGUAGE \"foo\" HANDLER \"plpgsql\" VALIDATOR test()"
20
+ ], statements)
21
+ end
22
+
23
+ def test_drop_language
24
+ Mig.drop_language(:foo)
25
+ Mig.drop_language(:foo, :if_exists => true, :cascade => true)
26
+
27
+ assert_equal([
28
+ "DROP PROCEDURAL LANGUAGE \"foo\"",
29
+ "DROP PROCEDURAL LANGUAGE IF EXISTS \"foo\" CASCADE"
30
+ ], statements)
31
+ end
32
+
33
+ def test_alter_language_name
34
+ Mig.alter_language_name(:foo, :bar)
35
+
36
+ assert_equal([
37
+ "ALTER PROCEDURAL LANGUAGE \"foo\" RENAME TO \"bar\""
38
+ ], statements)
39
+ end
40
+
41
+ def test_alter_language_owner
42
+ Mig.alter_language_owner(:foo, :bar)
43
+
44
+ assert_equal([
45
+ "ALTER PROCEDURAL LANGUAGE \"foo\" OWNER TO \"bar\""
46
+ ], statements)
47
+ end
48
+ end
@@ -0,0 +1,163 @@
1
+
2
+ $: << File.dirname(__FILE__)
3
+ require 'test_helper'
4
+
5
+ class PermissionsTests < Test::Unit::TestCase
6
+ include PostgreSQLExtensionsTestHelper
7
+
8
+ def test_grant_table_privileges
9
+ Mig.grant_table_privileges(:foo, :select, :nobody)
10
+ Mig.grant_table_privileges(:foo, [ :select, :update, :delete, :insert ], [ :nobody, :somebody ])
11
+ Mig.grant_table_privileges(:foo, :select, :nobody, :with_grant_option => true)
12
+ Mig.grant_table_privileges(:foo, :select, :nobody, :cascade => true)
13
+ Mig.grant_table_privileges(:foo, :select, :public, :cascade => true)
14
+
15
+ assert_equal([
16
+ "GRANT SELECT ON TABLE \"foo\" TO \"nobody\"",
17
+ "GRANT SELECT, UPDATE, DELETE, INSERT ON TABLE \"foo\" TO \"nobody\", \"somebody\"",
18
+ "GRANT SELECT ON TABLE \"foo\" TO \"nobody\" WITH GRANT OPTION",
19
+ "GRANT SELECT ON TABLE \"foo\" TO \"nobody\"",
20
+ "GRANT SELECT ON TABLE \"foo\" TO PUBLIC"
21
+ ], statements)
22
+ end
23
+
24
+ def test_revoke_table_privileges
25
+ Mig.revoke_table_privileges(:foo, :select, :nobody)
26
+ Mig.revoke_table_privileges(:foo, [ :select, :update, :delete, :insert ], [ :nobody, :somebody ])
27
+ Mig.revoke_table_privileges(:foo, :select, :nobody, :with_grant_option => true)
28
+ Mig.revoke_table_privileges(:foo, :select, :nobody, :cascade => true)
29
+ Mig.revoke_table_privileges(:foo, :select, :public, :cascade => true)
30
+
31
+ assert_equal([
32
+ "REVOKE SELECT ON TABLE \"foo\" FROM \"nobody\"",
33
+ "REVOKE SELECT, UPDATE, DELETE, INSERT ON TABLE \"foo\" FROM \"nobody\", \"somebody\"",
34
+ "REVOKE SELECT ON TABLE \"foo\" FROM \"nobody\"",
35
+ "REVOKE SELECT ON TABLE \"foo\" FROM \"nobody\" CASCADE",
36
+ "REVOKE SELECT ON TABLE \"foo\" FROM PUBLIC CASCADE"
37
+ ], statements)
38
+ end
39
+
40
+ def test_grant_sequence_privileges
41
+ Mig.grant_sequence_privileges(:foo, :select, :nobody)
42
+ Mig.grant_sequence_privileges(:foo, [ :select, :update ], [ :nobody, :somebody ])
43
+
44
+ assert_equal([
45
+ "GRANT SELECT ON SEQUENCE \"foo\" TO \"nobody\"",
46
+ "GRANT SELECT, UPDATE ON SEQUENCE \"foo\" TO \"nobody\", \"somebody\""
47
+ ], statements)
48
+ end
49
+
50
+ def test_revoke_sequence_privileges
51
+ Mig.revoke_sequence_privileges(:foo, :select, :nobody)
52
+ Mig.revoke_sequence_privileges(:foo, [ :select, :update ], [ :nobody, :somebody ])
53
+
54
+ assert_equal([
55
+ "REVOKE SELECT ON SEQUENCE \"foo\" FROM \"nobody\"",
56
+ "REVOKE SELECT, UPDATE ON SEQUENCE \"foo\" FROM \"nobody\", \"somebody\""
57
+ ], statements)
58
+ end
59
+
60
+ def test_grant_function_privileges
61
+ Mig.grant_function_privileges('test(text, integer)', :execute, :nobody)
62
+ Mig.grant_function_privileges('test(text, integer)', :all, [ :nobody, :somebody ])
63
+
64
+ assert_equal([
65
+ "GRANT EXECUTE ON FUNCTION test(text, integer) TO \"nobody\"",
66
+ "GRANT ALL ON FUNCTION test(text, integer) TO \"nobody\", \"somebody\""
67
+ ], statements)
68
+ end
69
+
70
+ def test_revoke_function_privileges
71
+ Mig.revoke_function_privileges('test(text, integer)', :execute, :nobody)
72
+ Mig.revoke_function_privileges('test(text, integer)', :all, [ :nobody, :somebody ])
73
+
74
+ assert_equal([
75
+ "REVOKE EXECUTE ON FUNCTION test(text, integer) FROM \"nobody\"",
76
+ "REVOKE ALL ON FUNCTION test(text, integer) FROM \"nobody\", \"somebody\""
77
+ ], statements)
78
+ end
79
+
80
+ def test_grant_language_privileges
81
+ Mig.grant_language_privileges('plpgsql', :usage, :nobody)
82
+ Mig.grant_language_privileges('plpgsql', :all, [ :nobody, :somebody ])
83
+
84
+ assert_equal([
85
+ "GRANT USAGE ON LANGUAGE \"plpgsql\" TO \"nobody\"",
86
+ "GRANT ALL ON LANGUAGE \"plpgsql\" TO \"nobody\", \"somebody\""
87
+ ], statements)
88
+ end
89
+
90
+ def test_revoke_language_privileges
91
+ Mig.revoke_language_privileges('plpgsql', :usage, :nobody)
92
+ Mig.revoke_language_privileges('plpgsql', :all, [ :nobody, :somebody ])
93
+
94
+ assert_equal([
95
+ "REVOKE USAGE ON LANGUAGE \"plpgsql\" FROM \"nobody\"",
96
+ "REVOKE ALL ON LANGUAGE \"plpgsql\" FROM \"nobody\", \"somebody\""
97
+ ], statements)
98
+ end
99
+
100
+ def test_grant_schema_privileges
101
+ Mig.grant_schema_privileges(:foo, :usage, :nobody)
102
+ Mig.grant_schema_privileges(:foo, :all, [ :nobody, :somebody ])
103
+
104
+ assert_equal([
105
+ "GRANT USAGE ON SCHEMA \"foo\" TO \"nobody\"",
106
+ "GRANT ALL ON SCHEMA \"foo\" TO \"nobody\", \"somebody\""
107
+ ], statements)
108
+ end
109
+
110
+ def test_revoke_schema_privileges
111
+ Mig.revoke_schema_privileges(:foo, :usage, :nobody)
112
+ Mig.revoke_schema_privileges(:foo, :all, [ :nobody, :somebody ])
113
+
114
+ assert_equal([
115
+ "REVOKE USAGE ON SCHEMA \"foo\" FROM \"nobody\"",
116
+ "REVOKE ALL ON SCHEMA \"foo\" FROM \"nobody\", \"somebody\""
117
+ ], statements)
118
+ end
119
+
120
+ def test_grant_tablespace_privileges
121
+ Mig.grant_tablespace_privileges(:foo, :create, :nobody)
122
+ Mig.grant_tablespace_privileges(:foo, :all, [ :nobody, :somebody ])
123
+
124
+ assert_equal([
125
+ "GRANT CREATE ON TABLESPACE \"foo\" TO \"nobody\"",
126
+ "GRANT ALL ON TABLESPACE \"foo\" TO \"nobody\", \"somebody\""
127
+ ], statements)
128
+ end
129
+
130
+ def test_revoke_tablespace_privileges
131
+ Mig.revoke_tablespace_privileges(:foo, :create, :nobody)
132
+ Mig.revoke_tablespace_privileges(:foo, :all, [ :nobody, :somebody ])
133
+
134
+ assert_equal([
135
+ "REVOKE CREATE ON TABLESPACE \"foo\" FROM \"nobody\"",
136
+ "REVOKE ALL ON TABLESPACE \"foo\" FROM \"nobody\", \"somebody\""
137
+ ], statements)
138
+ end
139
+
140
+ def test_grant_role_membership
141
+ Mig.grant_role_membership(:foo, :nobody)
142
+ Mig.grant_role_membership(:foo, [ :nobody, :somebody ])
143
+ Mig.grant_role_membership(:foo, [ :nobody, :somebody ], :with_admin_option => true)
144
+
145
+ assert_equal([
146
+ "GRANT \"foo\" TO \"nobody\"",
147
+ "GRANT \"foo\" TO \"nobody\", \"somebody\"",
148
+ "GRANT \"foo\" TO \"nobody\", \"somebody\" WITH ADMIN OPTION"
149
+ ], statements)
150
+ end
151
+
152
+ def test_revoke_role_membership
153
+ Mig.revoke_role_membership(:foo, :nobody)
154
+ Mig.revoke_role_membership(:foo, [ :nobody, :somebody ])
155
+ Mig.revoke_role_membership(:foo, [ :nobody, :somebody ], :with_admin_option => true)
156
+
157
+ assert_equal([
158
+ "REVOKE \"foo\" FROM \"nobody\"",
159
+ "REVOKE \"foo\" FROM \"nobody\", \"somebody\"",
160
+ "REVOKE \"foo\" FROM \"nobody\", \"somebody\""
161
+ ], statements)
162
+ end
163
+ end
@@ -0,0 +1,32 @@
1
+
2
+ $: << File.dirname(__FILE__)
3
+ require 'test_helper'
4
+
5
+ class RulesTests < Test::Unit::TestCase
6
+ include PostgreSQLExtensionsTestHelper
7
+
8
+ def test_create_rule
9
+ Mig.create_rule(
10
+ :ignore_root, :update, :users, :instead, :nothing, :conditions => 'user_id = 0'
11
+ )
12
+ Mig.create_rule(
13
+ :ignore_root, :update, :users, :instead, 'SELECT * FROM non_admins', {
14
+ :force => true,
15
+ :conditions => 'user_id > 0'
16
+ }
17
+ )
18
+
19
+ assert_equal([
20
+ "CREATE RULE \"ignore_root\" AS ON UPDATE TO \"users\" WHERE user_id = 0 DO INSTEAD NOTHING",
21
+ "CREATE OR REPLACE RULE \"ignore_root\" AS ON UPDATE TO \"users\" WHERE user_id > 0 DO INSTEAD SELECT * FROM non_admins"
22
+ ], statements)
23
+ end
24
+
25
+ def test_drop_rule
26
+ Mig.drop_rule(:foo, :bar)
27
+
28
+ assert_equal([
29
+ "DROP RULE \"foo\" ON \"bar\"",
30
+ ], statements)
31
+ end
32
+ end