gitlab-pg_query 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +321 -0
- data/LICENSE +28 -0
- data/README.md +180 -0
- data/Rakefile +23 -0
- data/ext/pg_query/extconf.rb +46 -0
- data/ext/pg_query/pg_query_ruby.c +129 -0
- data/ext/pg_query/pg_query_ruby.h +10 -0
- data/ext/pg_query/pg_query_ruby.sym +1 -0
- data/lib/pg_query.rb +16 -0
- data/lib/pg_query/deep_dup.rb +16 -0
- data/lib/pg_query/deparse.rb +1588 -0
- data/lib/pg_query/deparse/alter_table.rb +42 -0
- data/lib/pg_query/deparse/interval.rb +105 -0
- data/lib/pg_query/deparse/keywords.rb +159 -0
- data/lib/pg_query/deparse/rename.rb +41 -0
- data/lib/pg_query/filter_columns.rb +107 -0
- data/lib/pg_query/fingerprint.rb +115 -0
- data/lib/pg_query/legacy_parsetree.rb +109 -0
- data/lib/pg_query/node_types.rb +296 -0
- data/lib/pg_query/param_refs.rb +45 -0
- data/lib/pg_query/parse.rb +247 -0
- data/lib/pg_query/parse_error.rb +9 -0
- data/lib/pg_query/treewalker.rb +53 -0
- data/lib/pg_query/truncate.rb +60 -0
- data/lib/pg_query/version.rb +3 -0
- metadata +130 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
class PgQuery
|
2
|
+
module Deparse
|
3
|
+
module AlterTable
|
4
|
+
# Returns a list of strings of length one or length two. The first string
|
5
|
+
# will be placed before the column name and the second, if present, will be
|
6
|
+
# placed after.
|
7
|
+
#
|
8
|
+
# If node['subtype'] is the integer 4 (AT_DropNotNull),
|
9
|
+
# then return value of this method will be:
|
10
|
+
#
|
11
|
+
# ['ALTER COLUMN', 'DROP NOT NULL']
|
12
|
+
#
|
13
|
+
# Which will be composed into the SQL as:
|
14
|
+
#
|
15
|
+
# ALTER COLUMN {column_name} DROP NOT NULL
|
16
|
+
#
|
17
|
+
def self.commands(node)
|
18
|
+
action = ALTER_TABLE_TYPES_MAPPING[node['subtype']] || raise(format("Can't deparse: %s", node.inspect))
|
19
|
+
PgQuery::Deparse.instance_exec(node, &action)
|
20
|
+
end
|
21
|
+
|
22
|
+
ALTER_TABLE_TYPES_MAPPING = {
|
23
|
+
AT_AddColumn => ->(_node) { ['ADD COLUMN'] },
|
24
|
+
AT_ColumnDefault => ->(node) { ['ALTER COLUMN', node['def'] ? 'SET DEFAULT' : 'DROP DEFAULT'] },
|
25
|
+
AT_DropNotNull => ->(_node) { ['ALTER COLUMN', 'DROP NOT NULL'] },
|
26
|
+
AT_SetNotNull => ->(_node) { ['ALTER COLUMN', 'SET NOT NULL'] },
|
27
|
+
AT_SetStatistics => ->(_node) { ['ALTER COLUMN', 'SET STATISTICS'] },
|
28
|
+
AT_SetOptions => ->(_node) { ['ALTER COLUMN', 'SET'] },
|
29
|
+
AT_ResetOptions => ->(_node) { ['ALTER COLUMN', 'RESET'] },
|
30
|
+
AT_SetStorage => ->(_node) { ['ALTER COLUMN', 'SET STORAGE'] },
|
31
|
+
AT_DropColumn => ->(_node) { ['DROP'] },
|
32
|
+
AT_AddIndex => ->(_node) { ['ADD INDEX'] },
|
33
|
+
AT_AddConstraint => ->(_node) { ['ADD'] },
|
34
|
+
AT_AlterConstraint => ->(_node) { ['ALTER CONSTRAINT'] },
|
35
|
+
AT_ValidateConstraint => ->(_node) { ['VALIDATE CONSTRAINT'] },
|
36
|
+
AT_DropConstraint => ->(_node) { ['DROP CONSTRAINT'] },
|
37
|
+
AT_AlterColumnType => ->(_node) { ['ALTER COLUMN', 'TYPE'] },
|
38
|
+
AT_AlterColumnGenericOptions => ->(_node) { ['ALTER COLUMN', 'OPTIONS'] }
|
39
|
+
}.freeze
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
class PgQuery
|
2
|
+
module Deparse
|
3
|
+
module Interval
|
4
|
+
# A type called 'interval hour to minute' is stored in a compressed way by
|
5
|
+
# simplifying 'hour to minute' to a simple integer. This integer is computed
|
6
|
+
# by looking up the arbitrary number (always a power of two) for 'hour' and
|
7
|
+
# the one for 'minute' and XORing them together.
|
8
|
+
#
|
9
|
+
# For example, when parsing "interval hour to minute":
|
10
|
+
#
|
11
|
+
# HOUR_MASK = 10
|
12
|
+
# MINUTE_MASK = 11
|
13
|
+
# mask = (1 << 10) | (1 << 11)
|
14
|
+
# mask = 1024 | 2048
|
15
|
+
# mask = (010000000000
|
16
|
+
# xor
|
17
|
+
# 100000000000)
|
18
|
+
# mask = 110000000000
|
19
|
+
# mask = 3072
|
20
|
+
#
|
21
|
+
# Postgres will store this type as 'interval,3072'
|
22
|
+
# We deparse it by simply reversing that process.
|
23
|
+
#
|
24
|
+
def self.from_int(int)
|
25
|
+
SQL_BY_MASK[int]
|
26
|
+
end
|
27
|
+
|
28
|
+
# From src/include/utils/datetime.h
|
29
|
+
# The number is the power of 2 used for the mask.
|
30
|
+
MASKS = {
|
31
|
+
0 => 'RESERV',
|
32
|
+
1 => 'MONTH',
|
33
|
+
2 => 'YEAR',
|
34
|
+
3 => 'DAY',
|
35
|
+
4 => 'JULIAN',
|
36
|
+
5 => 'TZ',
|
37
|
+
6 => 'DTZ',
|
38
|
+
7 => 'DYNTZ',
|
39
|
+
8 => 'IGNORE_DTF',
|
40
|
+
9 => 'AMPM',
|
41
|
+
10 => 'HOUR',
|
42
|
+
11 => 'MINUTE',
|
43
|
+
12 => 'SECOND',
|
44
|
+
13 => 'MILLISECOND',
|
45
|
+
14 => 'MICROSECOND',
|
46
|
+
15 => 'DOY',
|
47
|
+
16 => 'DOW',
|
48
|
+
17 => 'UNITS',
|
49
|
+
18 => 'ADBC',
|
50
|
+
19 => 'AGO',
|
51
|
+
20 => 'ABS_BEFORE',
|
52
|
+
21 => 'ABS_AFTER',
|
53
|
+
22 => 'ISODATE',
|
54
|
+
23 => 'ISOTIME',
|
55
|
+
24 => 'WEEK',
|
56
|
+
25 => 'DECADE',
|
57
|
+
26 => 'CENTURY',
|
58
|
+
27 => 'MILLENNIUM',
|
59
|
+
28 => 'DTZMOD'
|
60
|
+
}.freeze
|
61
|
+
KEYS = MASKS.invert
|
62
|
+
|
63
|
+
# Postgres stores the interval 'day second' as 'day hour minute second' so
|
64
|
+
# we need to reconstruct the sql with only the largest and smallest time
|
65
|
+
# values. Since the rules for this are hardcoded in the grammar (and the
|
66
|
+
# above list is not sorted in any sensible way) it makes sense to hardcode
|
67
|
+
# the patterns here, too.
|
68
|
+
#
|
69
|
+
# This hash takes the form:
|
70
|
+
#
|
71
|
+
# { (1 << 1) | (1 << 2) => 'year to month' }
|
72
|
+
#
|
73
|
+
# Which is:
|
74
|
+
#
|
75
|
+
# { 6 => 'year to month' }
|
76
|
+
#
|
77
|
+
SQL_BY_MASK = {
|
78
|
+
(1 << KEYS['YEAR']) => %w[year],
|
79
|
+
(1 << KEYS['MONTH']) => %w[month],
|
80
|
+
(1 << KEYS['DAY']) => %w[day],
|
81
|
+
(1 << KEYS['HOUR']) => %w[hour],
|
82
|
+
(1 << KEYS['MINUTE']) => %w[minute],
|
83
|
+
(1 << KEYS['SECOND']) => %w[second],
|
84
|
+
(1 << KEYS['YEAR'] |
|
85
|
+
1 << KEYS['MONTH']) => %w[year month],
|
86
|
+
(1 << KEYS['DAY'] |
|
87
|
+
1 << KEYS['HOUR']) => %w[day hour],
|
88
|
+
(1 << KEYS['DAY'] |
|
89
|
+
1 << KEYS['HOUR'] |
|
90
|
+
1 << KEYS['MINUTE']) => %w[day minute],
|
91
|
+
(1 << KEYS['DAY'] |
|
92
|
+
1 << KEYS['HOUR'] |
|
93
|
+
1 << KEYS['MINUTE'] |
|
94
|
+
1 << KEYS['SECOND']) => %w[day second],
|
95
|
+
(1 << KEYS['HOUR'] |
|
96
|
+
1 << KEYS['MINUTE']) => %w[hour minute],
|
97
|
+
(1 << KEYS['HOUR'] |
|
98
|
+
1 << KEYS['MINUTE'] |
|
99
|
+
1 << KEYS['SECOND']) => %w[hour second],
|
100
|
+
(1 << KEYS['MINUTE'] |
|
101
|
+
1 << KEYS['SECOND']) => %w[minute second]
|
102
|
+
}.freeze
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
class PgQuery
|
2
|
+
module Deparse # rubocop:disable Metrics/ModuleLength
|
3
|
+
# Keywords that need to be escaped during deparsing. This matches they keywords
|
4
|
+
# excaped by quote_identifier in Postgres ruleutils.c. You can refresh this
|
5
|
+
# list using the kwlist.h file (make sure to ignore UNRESERVED_KEYWORD entries)
|
6
|
+
KEYWORDS = %w[
|
7
|
+
ALL
|
8
|
+
ANALYSE
|
9
|
+
ANALYZE
|
10
|
+
AND
|
11
|
+
ANY
|
12
|
+
ARRAY
|
13
|
+
AS
|
14
|
+
ASC
|
15
|
+
ASYMMETRIC
|
16
|
+
AUTHORIZATION
|
17
|
+
BETWEEN
|
18
|
+
BIGINT
|
19
|
+
BINARY
|
20
|
+
BIT
|
21
|
+
BOOLEAN
|
22
|
+
BOTH
|
23
|
+
CASE
|
24
|
+
CAST
|
25
|
+
CHAR
|
26
|
+
CHARACTER
|
27
|
+
CHECK
|
28
|
+
COALESCE
|
29
|
+
COLLATE
|
30
|
+
COLLATION
|
31
|
+
COLUMN
|
32
|
+
CONCURRENTLY
|
33
|
+
CONSTRAINT
|
34
|
+
CREATE
|
35
|
+
CROSS
|
36
|
+
CURRENT_CATALOG
|
37
|
+
CURRENT_DATE
|
38
|
+
CURRENT_ROLE
|
39
|
+
CURRENT_SCHEMA
|
40
|
+
CURRENT_TIME
|
41
|
+
CURRENT_TIMESTAMP
|
42
|
+
CURRENT_USER
|
43
|
+
DEC
|
44
|
+
DECIMAL
|
45
|
+
DEFAULT
|
46
|
+
DEFERRABLE
|
47
|
+
DESC
|
48
|
+
DISTINCT
|
49
|
+
DO
|
50
|
+
ELSE
|
51
|
+
END
|
52
|
+
EXCEPT
|
53
|
+
EXISTS
|
54
|
+
EXTRACT
|
55
|
+
FALSE
|
56
|
+
FETCH
|
57
|
+
FLOAT
|
58
|
+
FOR
|
59
|
+
FOREIGN
|
60
|
+
FREEZE
|
61
|
+
FROM
|
62
|
+
FULL
|
63
|
+
GRANT
|
64
|
+
GREATEST
|
65
|
+
GROUP
|
66
|
+
GROUPING
|
67
|
+
HAVING
|
68
|
+
ILIKE
|
69
|
+
IN
|
70
|
+
INITIALLY
|
71
|
+
INNER
|
72
|
+
INOUT
|
73
|
+
INT
|
74
|
+
INTEGER
|
75
|
+
INTERSECT
|
76
|
+
INTERVAL
|
77
|
+
INTO
|
78
|
+
IS
|
79
|
+
ISNULL
|
80
|
+
JOIN
|
81
|
+
LATERAL
|
82
|
+
LEADING
|
83
|
+
LEAST
|
84
|
+
LEFT
|
85
|
+
LIKE
|
86
|
+
LIMIT
|
87
|
+
LOCALTIME
|
88
|
+
LOCALTIMESTAMP
|
89
|
+
NATIONAL
|
90
|
+
NATURAL
|
91
|
+
NCHAR
|
92
|
+
NONE
|
93
|
+
NOT
|
94
|
+
NOTNULL
|
95
|
+
NULL
|
96
|
+
NULLIF
|
97
|
+
NUMERIC
|
98
|
+
OFFSET
|
99
|
+
ON
|
100
|
+
ONLY
|
101
|
+
OR
|
102
|
+
ORDER
|
103
|
+
OUT
|
104
|
+
OUTER
|
105
|
+
OVERLAPS
|
106
|
+
OVERLAY
|
107
|
+
PLACING
|
108
|
+
POSITION
|
109
|
+
PRECISION
|
110
|
+
PRIMARY
|
111
|
+
REAL
|
112
|
+
REFERENCES
|
113
|
+
RETURNING
|
114
|
+
RIGHT
|
115
|
+
ROW
|
116
|
+
SELECT
|
117
|
+
SESSION_USER
|
118
|
+
SETOF
|
119
|
+
SIMILAR
|
120
|
+
SMALLINT
|
121
|
+
SOME
|
122
|
+
SUBSTRING
|
123
|
+
SYMMETRIC
|
124
|
+
TABLE
|
125
|
+
TABLESAMPLE
|
126
|
+
THEN
|
127
|
+
TIME
|
128
|
+
TIMESTAMP
|
129
|
+
TO
|
130
|
+
TRAILING
|
131
|
+
TREAT
|
132
|
+
TRIM
|
133
|
+
TRUE
|
134
|
+
UNION
|
135
|
+
UNIQUE
|
136
|
+
USER
|
137
|
+
USING
|
138
|
+
VALUES
|
139
|
+
VARCHAR
|
140
|
+
VARIADIC
|
141
|
+
VERBOSE
|
142
|
+
WHEN
|
143
|
+
WHERE
|
144
|
+
WINDOW
|
145
|
+
WITH
|
146
|
+
XMLATTRIBUTES
|
147
|
+
XMLCONCAT
|
148
|
+
XMLELEMENT
|
149
|
+
XMLEXISTS
|
150
|
+
XMLFOREST
|
151
|
+
XMLNAMESPACES
|
152
|
+
XMLPARSE
|
153
|
+
XMLPI
|
154
|
+
XMLROOT
|
155
|
+
XMLSERIALIZE
|
156
|
+
XMLTABLE
|
157
|
+
].freeze
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class PgQuery
|
2
|
+
module Deparse
|
3
|
+
module Rename
|
4
|
+
# relation, subname and object is the array key in the node.
|
5
|
+
# Array return five value. First is the type like a TRIGGER, TABLE, DOMAIN
|
6
|
+
# Other values may be parameter or SQL key.
|
7
|
+
#
|
8
|
+
# If node['renameType'] is the integer 13 (OBJECT_TYPE_DOMCONSTRAINT),
|
9
|
+
# then return value of this method will be:
|
10
|
+
#
|
11
|
+
# %w[DOMAIN object RENAME CONSTRAINT subname]
|
12
|
+
#
|
13
|
+
# Which will be composed into the SQL as:
|
14
|
+
#
|
15
|
+
# ALTER {type} {name} RENAME CONSTRAINT {subname} TO {newname}
|
16
|
+
#
|
17
|
+
def self.commands(node)
|
18
|
+
action = RENAME_MAPPING[node['renameType']] || raise(format("Can't deparse: %s", node.inspect))
|
19
|
+
PgQuery::Deparse.instance_exec(node, &action)
|
20
|
+
end
|
21
|
+
|
22
|
+
RENAME_MAPPING = {
|
23
|
+
OBJECT_TYPE_CONVERSION => ->(_node) { %w[CONVERSION object RENAME] },
|
24
|
+
OBJECT_TYPE_TABLE => ->(_node) { %w[TABLE relation RENAME] },
|
25
|
+
OBJECT_TYPE_TABCONSTRAINT => ->(_node) { %w[TABLE relation RENAME CONSTRAINT subname] },
|
26
|
+
OBJECT_TYPE_INDEX => ->(_node) { %w[INDEX relation RENAME] },
|
27
|
+
OBJECT_TYPE_MATVIEW => ->(_node) { ['MATERIALIZED VIEW', 'relation', 'RENAME'] },
|
28
|
+
OBJECT_TYPE_TABLESPACE => ->(_node) { %w[TABLESPACE subname RENAME] },
|
29
|
+
OBJECT_TYPE_VIEW => ->(_node) { %w[VIEW relation RENAME] },
|
30
|
+
OBJECT_TYPE_COLUMN => ->(_node) { %w[TABLE relation RENAME COLUMN subname] },
|
31
|
+
OBJECT_TYPE_COLLATION => ->(_node) { %w[COLLATION object RENAME] },
|
32
|
+
OBJECT_TYPE_TYPE => ->(_node) { %w[TYPE object RENAME] },
|
33
|
+
OBJECT_TYPE_DOMCONSTRAINT => ->(_node) { %w[DOMAIN object RENAME CONSTRAINT subname] },
|
34
|
+
OBJECT_TYPE_RULE => ->(_node) { %w[RULE subname ON relation RENAME] },
|
35
|
+
OBJECT_TYPE_TRIGGER => ->(_node) { %w[TRIGGER subname ON relation RENAME] },
|
36
|
+
OBJECT_TYPE_AGGREGATE => ->(_node) { %w[AGGREGATE object RENAME] },
|
37
|
+
OBJECT_TYPE_FUNCTION => ->(_node) { %w[FUNCTION object RENAME] }
|
38
|
+
}.freeze
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
class PgQuery
|
2
|
+
# Returns a list of columns that the query filters by - this excludes the
|
3
|
+
# target list, but includes things like JOIN condition and WHERE clause.
|
4
|
+
#
|
5
|
+
# Note: This also traverses into sub-selects.
|
6
|
+
def filter_columns # rubocop:disable Metrics/CyclomaticComplexity
|
7
|
+
load_tables_and_aliases! if @aliases.nil?
|
8
|
+
|
9
|
+
# Get condition items from the parsetree
|
10
|
+
statements = @tree.dup
|
11
|
+
condition_items = []
|
12
|
+
filter_columns = []
|
13
|
+
loop do
|
14
|
+
statement = statements.shift
|
15
|
+
if statement
|
16
|
+
if statement[RAW_STMT]
|
17
|
+
statements << statement[RAW_STMT][STMT_FIELD]
|
18
|
+
elsif statement[SELECT_STMT]
|
19
|
+
case statement[SELECT_STMT]['op']
|
20
|
+
when 0
|
21
|
+
if statement[SELECT_STMT][FROM_CLAUSE_FIELD]
|
22
|
+
# FROM subselects
|
23
|
+
statement[SELECT_STMT][FROM_CLAUSE_FIELD].each do |item|
|
24
|
+
next unless item['RangeSubselect']
|
25
|
+
statements << item['RangeSubselect']['subquery']
|
26
|
+
end
|
27
|
+
|
28
|
+
# JOIN ON conditions
|
29
|
+
condition_items += conditions_from_join_clauses(statement[SELECT_STMT][FROM_CLAUSE_FIELD])
|
30
|
+
end
|
31
|
+
|
32
|
+
# WHERE clause
|
33
|
+
condition_items << statement[SELECT_STMT]['whereClause'] if statement[SELECT_STMT]['whereClause']
|
34
|
+
|
35
|
+
# CTEs
|
36
|
+
if statement[SELECT_STMT]['withClause']
|
37
|
+
statement[SELECT_STMT]['withClause']['WithClause']['ctes'].each do |item|
|
38
|
+
statements << item['CommonTableExpr']['ctequery'] if item['CommonTableExpr']
|
39
|
+
end
|
40
|
+
end
|
41
|
+
when 1
|
42
|
+
statements << statement[SELECT_STMT]['larg'] if statement[SELECT_STMT]['larg']
|
43
|
+
statements << statement[SELECT_STMT]['rarg'] if statement[SELECT_STMT]['rarg']
|
44
|
+
end
|
45
|
+
elsif statement['UpdateStmt']
|
46
|
+
condition_items << statement['UpdateStmt']['whereClause'] if statement['UpdateStmt']['whereClause']
|
47
|
+
elsif statement['DeleteStmt']
|
48
|
+
condition_items << statement['DeleteStmt']['whereClause'] if statement['DeleteStmt']['whereClause']
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Process both JOIN and WHERE conditions here
|
53
|
+
next_item = condition_items.shift
|
54
|
+
if next_item
|
55
|
+
if next_item[A_EXPR]
|
56
|
+
%w[lexpr rexpr].each do |side|
|
57
|
+
expr = next_item.values[0][side]
|
58
|
+
next unless expr && expr.is_a?(Hash)
|
59
|
+
condition_items << expr
|
60
|
+
end
|
61
|
+
elsif next_item[BOOL_EXPR]
|
62
|
+
condition_items += next_item[BOOL_EXPR]['args']
|
63
|
+
elsif next_item[ROW_EXPR]
|
64
|
+
condition_items += next_item[ROW_EXPR]['args']
|
65
|
+
elsif next_item[COLUMN_REF]
|
66
|
+
column, table = next_item[COLUMN_REF]['fields'].map { |f| f['String']['str'] }.reverse
|
67
|
+
filter_columns << [@aliases[table] || table, column]
|
68
|
+
elsif next_item[NULL_TEST]
|
69
|
+
condition_items << next_item[NULL_TEST]['arg']
|
70
|
+
elsif next_item[BOOLEAN_TEST]
|
71
|
+
condition_items << next_item[BOOLEAN_TEST]['arg']
|
72
|
+
elsif next_item[FUNC_CALL]
|
73
|
+
# FIXME: This should actually be extracted as a funccall and be compared with those indices
|
74
|
+
condition_items += next_item[FUNC_CALL]['args'] if next_item[FUNC_CALL]['args']
|
75
|
+
elsif next_item[SUB_LINK]
|
76
|
+
condition_items << next_item[SUB_LINK]['testexpr']
|
77
|
+
statements << next_item[SUB_LINK]['subselect']
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
break if statements.empty? && condition_items.empty?
|
82
|
+
end
|
83
|
+
|
84
|
+
filter_columns.uniq
|
85
|
+
end
|
86
|
+
|
87
|
+
protected
|
88
|
+
|
89
|
+
def conditions_from_join_clauses(from_clause)
|
90
|
+
condition_items = []
|
91
|
+
from_clause.each do |item|
|
92
|
+
next unless item[JOIN_EXPR]
|
93
|
+
|
94
|
+
joinexpr_items = [item[JOIN_EXPR]]
|
95
|
+
loop do
|
96
|
+
next_item = joinexpr_items.shift
|
97
|
+
break unless next_item
|
98
|
+
condition_items << next_item['quals'] if next_item['quals']
|
99
|
+
%w[larg rarg].each do |side|
|
100
|
+
next unless next_item[side][JOIN_EXPR]
|
101
|
+
joinexpr_items << next_item[side][JOIN_EXPR]
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
condition_items
|
106
|
+
end
|
107
|
+
end
|