gitlab-pg_query 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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