gitlab-pg_query 1.3.0

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.
@@ -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