1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
# frozen_string_literal: true
module Gitlab
module Database
module SchemaValidation
module Adapters
UndefinedPGType = Class.new(StandardError)
class ColumnStructureSqlAdapter
NOT_NULL_CONSTR = :CONSTR_NOTNULL
DEFAULT_CONSTR = :CONSTR_DEFAULT
MAPPINGS = {
't' => 'true',
'f' => 'false'
}.freeze
attr_reader :table_name
def initialize(table_name, pg_query_stmt, partitioning_stmt)
@table_name = table_name
@pg_query_stmt = pg_query_stmt
@partitioning_stmt = partitioning_stmt
end
def name
@name ||= pg_query_stmt.colname
end
def data_type
type(pg_query_stmt.type_name)
end
def default
return if name == 'id'
value = parse_node(constraints.find { |node| node.constraint.contype == DEFAULT_CONSTR })
return unless value
"DEFAULT #{value}"
end
def nullable
'NOT NULL' if constraints.any? { |node| node.constraint.contype == NOT_NULL_CONSTR }
end
def partition_key?
partition_keys.include?(name)
end
private
attr_reader :pg_query_stmt, :partitioning_stmt
def constraints
@constraints ||= pg_query_stmt.constraints
end
# Returns the node type
#
# pg_type:: type alias, used internally by postgres, +int4+, +int8+, +bool+, +varchar+
# type:: type name, like +integer+, +bigint+, +boolean+, +character varying+.
# array_ext:: adds the +[]+ extension for array types.
# precision_ext:: adds the precision, if have any, like +(255)+, +(6)+.
#
# @info +timestamp+ and +timestamptz+ have a particular case when precision is defined.
# In this case, the order of the statement needs to be re-arranged from
# timestamp without time zone(6) to timestamp(6) without a time zone.
def type(node)
pg_type = parse_node(node.names.last)
type = PgTypes::TYPES.fetch(pg_type).dup
array_ext = '[]' if node.array_bounds.any?
precision_ext = "(#{node.typmods.map { |typmod| parse_node(typmod) }.join(',')})" if node.typmods.any?
if %w[timestamp timestamptz].include?(pg_type)
type.gsub!('timestamp', ['timestamp', precision_ext].compact.join(''))
precision_ext = nil
end
[type, precision_ext, array_ext].compact.join('')
rescue KeyError => exception
raise UndefinedPGType, exception.message
end
# Parses PGQuery nodes recursively
#
# :constraint:: nodes that groups column default info
# :partition_elem:: node that store partition key info
# :func_cal:: nodes that stores functions, like +now()+
# :a_const:: nodes that stores constant values, like +t+, +f+, +0.0.0.0+, +255+, +1.0+
# :type_cast:: nodes that stores casting values, like +'name'::text+, +'0.0.0.0'::inet+
# else:: extract node values in the last iteration of the recursion, like +int4+, +1.0+, +now+, +255+
#
# @note boolean types types are mapped from +t+, +f+ to +true+, +false+
def parse_node(node)
return unless node
case node.node
when :constraint
parse_node(node.constraint.raw_expr)
when :partition_elem
node.partition_elem.name
when :func_call
"#{parse_node(node.func_call.funcname.first)}()"
when :a_const
parse_node(node.a_const.val)
when :type_cast
value = parse_node(node.type_cast.arg)
type = type(node.type_cast.type_name)
separator = MAPPINGS.key?(value) ? '' : "::#{type}"
[MAPPINGS.fetch(value, "'#{value}'"), separator].compact.join('')
else
node.to_h[node.node].values.last
end
end
def partition_keys
return [] unless partitioning_stmt
@partition_keys ||= partitioning_stmt.part_params.map { |key_stmt| parse_node(key_stmt) }
end
end
end
end
end
end
|