activerecord-materialize-adapter 0.2.0 → 0.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.
- checksums.yaml +4 -4
- data/lib/active_record/connection_adapters/materialize/column.rb +0 -9
- data/lib/active_record/connection_adapters/materialize/database_statements.rb +22 -41
- data/lib/active_record/connection_adapters/materialize/oid.rb +0 -12
- data/lib/active_record/connection_adapters/materialize/oid/type_map_initializer.rb +5 -48
- data/lib/active_record/connection_adapters/materialize/quoting.rb +1 -7
- data/lib/active_record/connection_adapters/materialize/schema/source_statements.rb +66 -0
- data/lib/active_record/connection_adapters/materialize/schema/view_statements.rb +22 -0
- data/lib/active_record/connection_adapters/materialize/schema_creation.rb +18 -53
- data/lib/active_record/connection_adapters/materialize/schema_definitions.rb +3 -6
- data/lib/active_record/connection_adapters/materialize/schema_dumper.rb +82 -22
- data/lib/active_record/connection_adapters/materialize/schema_statements.rb +102 -354
- data/lib/active_record/connection_adapters/materialize/version.rb +1 -1
- data/lib/active_record/connection_adapters/materialize_adapter.rb +100 -253
- metadata +9 -15
- data/lib/active_record/connection_adapters/materialize/oid/cidr.rb +0 -50
- data/lib/active_record/connection_adapters/materialize/oid/legacy_point.rb +0 -44
- data/lib/active_record/connection_adapters/materialize/oid/money.rb +0 -41
- data/lib/active_record/connection_adapters/materialize/oid/point.rb +0 -64
- data/lib/active_record/connection_adapters/materialize/oid/range.rb +0 -96
- data/lib/active_record/connection_adapters/materialize/oid/uuid.rb +0 -25
- data/lib/active_record/connection_adapters/materialize/oid/vector.rb +0 -28
- data/lib/active_record/connection_adapters/materialize/oid/xml.rb +0 -30
@@ -174,10 +174,7 @@ module ActiveRecord
|
|
174
174
|
# :call-seq: xml(*names, **options)
|
175
175
|
|
176
176
|
included do
|
177
|
-
define_column_methods :
|
178
|
-
:hstore, :inet, :interval, :int4range, :int8range, :jsonb, :ltree, :macaddr,
|
179
|
-
:money, :numrange, :oid, :point, :line, :lseg, :box, :path, :polygon, :circle,
|
180
|
-
:serial, :tsrange, :tstzrange, :tsvector, :uuid, :xml
|
177
|
+
define_column_methods :bigint, :bit, :bit_varying, :interval, :jsonb, :oid
|
181
178
|
end
|
182
179
|
end
|
183
180
|
|
@@ -194,9 +191,9 @@ module ActiveRecord
|
|
194
191
|
private
|
195
192
|
def integer_like_primary_key_type(type, options)
|
196
193
|
if type == :bigint || options[:limit] == 8
|
197
|
-
:
|
194
|
+
:bigint
|
198
195
|
else
|
199
|
-
:
|
196
|
+
:integer
|
200
197
|
end
|
201
198
|
end
|
202
199
|
end
|
@@ -4,18 +4,88 @@ module ActiveRecord
|
|
4
4
|
module ConnectionAdapters
|
5
5
|
module Materialize
|
6
6
|
class SchemaDumper < ActiveRecord::ConnectionAdapters::SchemaDumper # :nodoc:
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
7
|
+
def dump(stream)
|
8
|
+
header(stream)
|
9
|
+
extensions(stream)
|
10
|
+
tables(stream)
|
11
|
+
views_and_sources(stream)
|
12
|
+
trailer(stream)
|
13
|
+
stream
|
14
|
+
end
|
15
|
+
|
16
|
+
def views_and_sources(stream)
|
17
|
+
sorted_sources = @connection.sources.sort
|
18
|
+
sorted_sources.each do |source_name|
|
19
|
+
source(source_name, stream)
|
20
|
+
end
|
21
|
+
|
22
|
+
# TODO sort views by dependencies
|
23
|
+
sorted_views = @connection.views.sort
|
24
|
+
sorted_views.each do |view_name|
|
25
|
+
view(view_name, stream)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Create source
|
30
|
+
def source(source_name, stream)
|
31
|
+
options = @connection.source_options(source_name)
|
32
|
+
begin
|
33
|
+
src_stream = StringIO.new
|
34
|
+
|
35
|
+
src_stream.print " create_source #{remove_prefix_and_suffix(source_name).inspect}"
|
36
|
+
src_stream.print ", source_type: :#{options[:source_type]}" unless options[:source_type].nil?
|
37
|
+
src_stream.puts ", publication: #{options[:publication].inspect}" unless options[:publication].nil?
|
38
|
+
src_stream.puts ", materialize: #{options[:materialize]}" unless options[:materialize].nil?
|
39
|
+
src_stream.puts
|
40
|
+
|
41
|
+
src_stream.rewind
|
42
|
+
stream.print src_stream.read
|
43
|
+
|
44
|
+
rescue StandardError => e
|
45
|
+
stream.puts "# Could not dump source #{source_name.inspect} because of following #{e.class}"
|
46
|
+
stream.puts "# #{e.message}"
|
47
|
+
stream.puts
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Create view
|
52
|
+
def view(view_name, stream)
|
53
|
+
creation_query = @connection.view_sql(view_name)
|
54
|
+
begin
|
55
|
+
src_stream = StringIO.new
|
56
|
+
src_stream.puts " create_view #{remove_prefix_and_suffix(view_name).inspect} do"
|
57
|
+
|
58
|
+
src_stream.puts " <<-SQL.squish"
|
59
|
+
src_stream.puts " #{creation_query}"
|
60
|
+
src_stream.puts " SQL"
|
61
|
+
|
62
|
+
src_stream.puts " end"
|
63
|
+
src_stream.puts
|
64
|
+
|
65
|
+
# TODO indexes
|
66
|
+
|
67
|
+
src_stream.rewind
|
68
|
+
stream.print src_stream.read
|
69
|
+
|
70
|
+
rescue StandardError => e
|
71
|
+
stream.puts "# Could not dump source #{source_name.inspect} because of following #{e.class}"
|
72
|
+
stream.puts "# #{e.message}"
|
73
|
+
stream.puts
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def indexes_in_create(table, stream)
|
78
|
+
if (indexes = @connection.indexes(table)).any?
|
79
|
+
index_statements = indexes
|
80
|
+
.select { |index| !index.name.include?("_primary_idx") }
|
81
|
+
.map do |index|
|
82
|
+
" t.index #{index_parts(index).join(', ')}"
|
14
83
|
end
|
15
|
-
|
16
|
-
end
|
84
|
+
stream.puts index_statements.sort.join("\n")
|
17
85
|
end
|
86
|
+
end
|
18
87
|
|
88
|
+
private
|
19
89
|
def prepare_column_options(column)
|
20
90
|
spec = super
|
21
91
|
spec[:array] = "true" if column.array?
|
@@ -23,26 +93,16 @@ module ActiveRecord
|
|
23
93
|
end
|
24
94
|
|
25
95
|
def default_primary_key?(column)
|
26
|
-
schema_type(column) == :
|
27
|
-
end
|
28
|
-
|
29
|
-
def explicit_primary_key_default?(column)
|
30
|
-
column.type == :uuid || (column.type == :integer && !column.serial?)
|
96
|
+
schema_type(column) == :bigint
|
31
97
|
end
|
32
98
|
|
33
99
|
def schema_type(column)
|
34
|
-
return super unless column.serial?
|
35
|
-
|
36
100
|
if column.bigint?
|
37
|
-
:
|
101
|
+
:bigint
|
38
102
|
else
|
39
|
-
|
103
|
+
super
|
40
104
|
end
|
41
105
|
end
|
42
|
-
|
43
|
-
def schema_expression(column)
|
44
|
-
super unless column.serial?
|
45
|
-
end
|
46
106
|
end
|
47
107
|
end
|
48
108
|
end
|
@@ -40,90 +40,36 @@ module ActiveRecord
|
|
40
40
|
|
41
41
|
# Returns true if schema exists.
|
42
42
|
def schema_exists?(name)
|
43
|
-
|
43
|
+
schema_names.include? name.to_s
|
44
44
|
end
|
45
45
|
|
46
46
|
# Verifies existence of an index with a given name.
|
47
47
|
def index_name_exists?(table_name, index_name)
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
query_value(<<~SQL, "SCHEMA").to_i > 0
|
52
|
-
SELECT COUNT(*)
|
53
|
-
FROM pg_class t
|
54
|
-
INNER JOIN pg_index d ON t.oid = d.indrelid
|
55
|
-
INNER JOIN pg_class i ON d.indexrelid = i.oid
|
56
|
-
LEFT JOIN pg_namespace n ON n.oid = i.relnamespace
|
57
|
-
WHERE i.relkind IN ('i', 'I')
|
58
|
-
AND i.relname = #{index[:name]}
|
59
|
-
AND t.relname = #{table[:name]}
|
60
|
-
AND n.nspname = #{index[:schema]}
|
48
|
+
available_indexes = execute(<<~SQL, "SCHEMA")
|
49
|
+
SHOW INDEXES FROM #{quote_table_name(table_name)}
|
61
50
|
SQL
|
51
|
+
available_indexes.map { |c| c['key_name'] }.include? index_name.to_s
|
62
52
|
end
|
63
53
|
|
64
54
|
# Returns an array of indexes for the given table.
|
65
55
|
def indexes(table_name) # :nodoc:
|
66
|
-
|
67
|
-
|
68
|
-
result = query(<<~SQL, "SCHEMA")
|
69
|
-
SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
|
70
|
-
pg_catalog.obj_description(i.oid, 'pg_class') AS comment
|
71
|
-
FROM pg_class t
|
72
|
-
INNER JOIN pg_index d ON t.oid = d.indrelid
|
73
|
-
INNER JOIN pg_class i ON d.indexrelid = i.oid
|
74
|
-
LEFT JOIN pg_namespace n ON n.oid = i.relnamespace
|
75
|
-
WHERE i.relkind IN ('i', 'I')
|
76
|
-
AND d.indisprimary = 'f'
|
77
|
-
AND t.relname = #{scope[:name]}
|
78
|
-
AND n.nspname = #{scope[:schema]}
|
79
|
-
ORDER BY i.relname
|
56
|
+
available_indexes = execute(<<~SQL, "SCHEMA")
|
57
|
+
SHOW INDEXES FROM #{quote_table_name(table_name)}
|
80
58
|
SQL
|
81
|
-
|
82
|
-
|
83
|
-
index_name = row[0]
|
84
|
-
unique = row[1]
|
85
|
-
indkey = row[2].split(" ").map(&:to_i)
|
86
|
-
inddef = row[3]
|
87
|
-
oid = row[4]
|
88
|
-
comment = row[5]
|
89
|
-
|
90
|
-
using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/m).flatten
|
91
|
-
|
92
|
-
orders = {}
|
93
|
-
opclasses = {}
|
94
|
-
|
95
|
-
if indkey.include?(0)
|
96
|
-
columns = expressions
|
97
|
-
else
|
98
|
-
columns = Hash[query(<<~SQL, "SCHEMA")].values_at(*indkey).compact
|
99
|
-
SELECT a.attnum, a.attname
|
100
|
-
FROM pg_attribute a
|
101
|
-
WHERE a.attrelid = #{oid}
|
102
|
-
AND a.attnum IN (#{indkey.join(",")})
|
103
|
-
SQL
|
104
|
-
|
105
|
-
# add info on sort order (only desc order is explicitly specified, asc is the default)
|
106
|
-
# and non-default opclasses
|
107
|
-
expressions.scan(/(?<column>\w+)"?\s?(?<opclass>\w+_ops)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls|
|
108
|
-
opclasses[column] = opclass.to_sym if opclass
|
109
|
-
if nulls
|
110
|
-
orders[column] = [desc, nulls].compact.join(" ")
|
111
|
-
else
|
112
|
-
orders[column] = :desc if desc
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
59
|
+
available_indexes.group_by { |c| c['key_name'] }.map do |k, v|
|
60
|
+
index_name = k
|
117
61
|
IndexDefinition.new(
|
118
62
|
table_name,
|
119
63
|
index_name,
|
120
|
-
unique,
|
121
|
-
columns,
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
64
|
+
unique = false,
|
65
|
+
columns = v.map { |c| c['column_name'] },
|
66
|
+
lengths: {},
|
67
|
+
orders: {},
|
68
|
+
opclasses: {},
|
69
|
+
where: nil,
|
70
|
+
type: nil,
|
71
|
+
using: nil,
|
72
|
+
comment: nil
|
127
73
|
)
|
128
74
|
end
|
129
75
|
end
|
@@ -149,9 +95,9 @@ module ActiveRecord
|
|
149
95
|
end
|
150
96
|
end
|
151
97
|
|
152
|
-
#
|
98
|
+
# Unsupported current_database() use default config
|
153
99
|
def current_database
|
154
|
-
|
100
|
+
@config[:database]
|
155
101
|
end
|
156
102
|
|
157
103
|
# Returns the current schema name.
|
@@ -159,52 +105,41 @@ module ActiveRecord
|
|
159
105
|
query_value("SELECT current_schema", "SCHEMA")
|
160
106
|
end
|
161
107
|
|
162
|
-
#
|
108
|
+
# Get encoding of database
|
163
109
|
def encoding
|
164
|
-
query_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database
|
110
|
+
query_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = #{quote(current_database)}", "SCHEMA")
|
165
111
|
end
|
166
112
|
|
167
|
-
#
|
113
|
+
# Unsupported collation
|
168
114
|
def collation
|
169
|
-
|
115
|
+
nil
|
170
116
|
end
|
171
117
|
|
172
|
-
#
|
118
|
+
# Get ctype
|
173
119
|
def ctype
|
174
|
-
query_value("SELECT datctype FROM pg_database WHERE datname = current_database
|
120
|
+
query_value("SELECT datctype FROM pg_database WHERE datname = #{quote(current_database)}", "SCHEMA")
|
175
121
|
end
|
176
122
|
|
177
123
|
# Returns an array of schema names.
|
178
124
|
def schema_names
|
179
125
|
query_values(<<~SQL, "SCHEMA")
|
180
|
-
SELECT
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
126
|
+
SELECT
|
127
|
+
s.name
|
128
|
+
FROM mz_schemas s
|
129
|
+
LEFT JOIN mz_databases d ON s.database_id = d.id
|
130
|
+
WHERE
|
131
|
+
d.name = #{quote(current_database)}
|
185
132
|
SQL
|
186
133
|
end
|
187
134
|
|
188
135
|
# Creates a schema for the given schema name.
|
189
136
|
def create_schema(schema_name)
|
190
|
-
execute "CREATE SCHEMA #{quote_schema_name(schema_name)}"
|
137
|
+
execute "CREATE SCHEMA #{quote_schema_name(schema_name.to_s)}"
|
191
138
|
end
|
192
139
|
|
193
140
|
# Drops the schema for the given schema name.
|
194
141
|
def drop_schema(schema_name, options = {})
|
195
|
-
execute "DROP SCHEMA#{' IF EXISTS' if options[:if_exists]} #{quote_schema_name(schema_name)} CASCADE"
|
196
|
-
end
|
197
|
-
|
198
|
-
# Sets the schema search path to a string of comma-separated schema names.
|
199
|
-
# Names beginning with $ have to be quoted (e.g. $user => '$user').
|
200
|
-
# See: https://www.postgresql.org/docs/current/static/ddl-schemas.html
|
201
|
-
#
|
202
|
-
# This should be not be called manually but set in database.yml.
|
203
|
-
def schema_search_path=(schema_csv)
|
204
|
-
if schema_csv
|
205
|
-
execute("SET search_path TO #{schema_csv}", "SCHEMA")
|
206
|
-
@schema_search_path = schema_csv
|
207
|
-
end
|
142
|
+
execute "DROP SCHEMA#{' IF EXISTS' if options[:if_exists]} #{quote_schema_name(schema_name.to_s)} CASCADE"
|
208
143
|
end
|
209
144
|
|
210
145
|
# Returns the active schema search path.
|
@@ -212,170 +147,41 @@ module ActiveRecord
|
|
212
147
|
@schema_search_path ||= query_value("SHOW search_path", "SCHEMA")
|
213
148
|
end
|
214
149
|
|
215
|
-
# Returns the
|
216
|
-
def client_min_messages
|
217
|
-
query_value("SHOW client_min_messages", "SCHEMA")
|
218
|
-
end
|
219
|
-
|
220
|
-
# Set the client message level.
|
221
|
-
def client_min_messages=(level)
|
222
|
-
execute("SET client_min_messages TO '#{level}'", "SCHEMA")
|
223
|
-
end
|
224
|
-
|
225
|
-
# Returns the sequence name for a table's primary key or some other specified key.
|
150
|
+
# Returns the sequence name for compatability
|
226
151
|
def default_sequence_name(table_name, pk = "id") #:nodoc:
|
227
|
-
result = serial_sequence(table_name, pk)
|
228
|
-
return nil unless result
|
229
|
-
Utils.extract_schema_qualified_name(result).to_s
|
230
|
-
rescue ActiveRecord::StatementInvalid
|
231
152
|
Materialize::Name.new(nil, "#{table_name}_#{pk}_seq").to_s
|
232
153
|
end
|
233
154
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
# Sets the sequence of a table's primary key to the specified value.
|
239
|
-
def set_pk_sequence!(table, value) #:nodoc:
|
240
|
-
pk, sequence = pk_and_sequence_for(table)
|
241
|
-
|
242
|
-
if pk
|
243
|
-
if sequence
|
244
|
-
quoted_sequence = quote_table_name(sequence)
|
245
|
-
|
246
|
-
query_value("SELECT setval(#{quote(quoted_sequence)}, #{value})", "SCHEMA")
|
247
|
-
else
|
248
|
-
@logger.warn "#{table} has primary key #{pk} with no default sequence." if @logger
|
249
|
-
end
|
250
|
-
end
|
251
|
-
end
|
252
|
-
|
253
|
-
# Resets the sequence of a table's primary key to the maximum value.
|
254
|
-
def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
|
255
|
-
unless pk && sequence
|
256
|
-
default_pk, default_sequence = pk_and_sequence_for(table)
|
257
|
-
|
258
|
-
pk ||= default_pk
|
259
|
-
sequence ||= default_sequence
|
260
|
-
end
|
261
|
-
|
262
|
-
if @logger && pk && !sequence
|
263
|
-
@logger.warn "#{table} has primary key #{pk} with no default sequence."
|
264
|
-
end
|
265
|
-
|
266
|
-
if pk && sequence
|
267
|
-
quoted_sequence = quote_table_name(sequence)
|
268
|
-
max_pk = query_value("SELECT MAX(#{quote_column_name pk}) FROM #{quote_table_name(table)}", "SCHEMA")
|
269
|
-
if max_pk.nil?
|
270
|
-
if database_version >= 100000
|
271
|
-
minvalue = query_value("SELECT seqmin FROM pg_sequence WHERE seqrelid = #{quote(quoted_sequence)}::regclass", "SCHEMA")
|
272
|
-
else
|
273
|
-
minvalue = query_value("SELECT min_value FROM #{quoted_sequence}", "SCHEMA")
|
274
|
-
end
|
275
|
-
end
|
276
|
-
|
277
|
-
query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk ? max_pk : minvalue}, #{max_pk ? true : false})", "SCHEMA")
|
278
|
-
end
|
279
|
-
end
|
280
|
-
|
281
|
-
# Returns a table's primary key and belonging sequence.
|
282
|
-
def pk_and_sequence_for(table) #:nodoc:
|
283
|
-
# First try looking for a sequence with a dependency on the
|
284
|
-
# given table's primary key.
|
285
|
-
result = query(<<~SQL, "SCHEMA")[0]
|
286
|
-
SELECT attr.attname, nsp.nspname, seq.relname
|
287
|
-
FROM pg_class seq,
|
288
|
-
pg_attribute attr,
|
289
|
-
pg_depend dep,
|
290
|
-
pg_constraint cons,
|
291
|
-
pg_namespace nsp
|
292
|
-
WHERE seq.oid = dep.objid
|
293
|
-
AND seq.relkind = 'S'
|
294
|
-
AND attr.attrelid = dep.refobjid
|
295
|
-
AND attr.attnum = dep.refobjsubid
|
296
|
-
AND attr.attrelid = cons.conrelid
|
297
|
-
AND attr.attnum = cons.conkey[1]
|
298
|
-
AND seq.relnamespace = nsp.oid
|
299
|
-
AND cons.contype = 'p'
|
300
|
-
AND dep.classid = 'pg_class'::regclass
|
301
|
-
AND dep.refobjid = #{quote(quote_table_name(table))}::regclass
|
155
|
+
# Primary keys - approximation
|
156
|
+
def primary_keys(table_name) # :nodoc:
|
157
|
+
available_indexes = execute(<<~SQL, "SCHEMA").group_by { |c| c['key_name'] }
|
158
|
+
SHOW INDEXES FROM #{quote_table_name(table_name)}
|
302
159
|
SQL
|
303
|
-
|
304
|
-
if
|
305
|
-
|
306
|
-
SELECT attr.attname, nsp.nspname,
|
307
|
-
CASE
|
308
|
-
WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL
|
309
|
-
WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN
|
310
|
-
substr(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2),
|
311
|
-
strpos(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), '.')+1)
|
312
|
-
ELSE split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2)
|
313
|
-
END
|
314
|
-
FROM pg_class t
|
315
|
-
JOIN pg_attribute attr ON (t.oid = attrelid)
|
316
|
-
JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
|
317
|
-
JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
|
318
|
-
JOIN pg_namespace nsp ON (t.relnamespace = nsp.oid)
|
319
|
-
WHERE t.oid = #{quote(quote_table_name(table))}::regclass
|
320
|
-
AND cons.contype = 'p'
|
321
|
-
AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate'
|
322
|
-
SQL
|
323
|
-
end
|
324
|
-
|
325
|
-
pk = result.shift
|
326
|
-
if result.last
|
327
|
-
[pk, Materialize::Name.new(*result)]
|
160
|
+
possible_primary = available_indexes.keys.select { |c| c.include? "primary_idx" }
|
161
|
+
if possible_primary.size == 1
|
162
|
+
available_indexes[possible_primary.first].map { |c| c['column_name'] }
|
328
163
|
else
|
329
|
-
[
|
164
|
+
[]
|
330
165
|
end
|
331
|
-
rescue
|
332
|
-
nil
|
333
166
|
end
|
334
167
|
|
335
|
-
|
336
|
-
query_values(<<~SQL, "SCHEMA")
|
337
|
-
SELECT a.attname
|
338
|
-
FROM (
|
339
|
-
SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx
|
340
|
-
FROM pg_index
|
341
|
-
WHERE indrelid = #{quote(quote_table_name(table_name))}::regclass
|
342
|
-
AND indisprimary
|
343
|
-
) i
|
344
|
-
JOIN pg_attribute a
|
345
|
-
ON a.attrelid = i.indrelid
|
346
|
-
AND a.attnum = i.indkey[i.idx]
|
347
|
-
ORDER BY i.idx
|
348
|
-
SQL
|
349
|
-
end
|
350
|
-
|
351
|
-
# Renames a table.
|
352
|
-
# Also renames a table's primary key sequence if the sequence name exists and
|
353
|
-
# matches the Active Record default.
|
354
|
-
#
|
355
|
-
# Example:
|
356
|
-
# rename_table('octopuses', 'octopi')
|
168
|
+
# Renames a table with index references
|
357
169
|
def rename_table(table_name, new_name)
|
358
170
|
clear_cache!
|
359
171
|
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
|
360
|
-
|
361
|
-
|
362
|
-
idx = "#{table_name}_pkey"
|
363
|
-
new_idx = "#{new_name}_pkey"
|
364
|
-
execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}"
|
365
|
-
if seq && seq.identifier == "#{table_name}_#{pk}_seq"
|
366
|
-
new_seq = "#{new_name}_#{pk}_seq"
|
367
|
-
execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}"
|
368
|
-
end
|
172
|
+
if index_name_exists?(new_name, "#{table_name}_primary_idx")
|
173
|
+
rename_index(new_name, "#{table_name}_primary_idx", "#{new_name}_primary_idx")
|
369
174
|
end
|
370
175
|
rename_table_indexes(table_name, new_name)
|
371
176
|
end
|
372
177
|
|
178
|
+
# https://materialize.com/docs/sql/alter-rename/
|
373
179
|
def add_column(table_name, column_name, type, **options) #:nodoc:
|
374
180
|
clear_cache!
|
375
181
|
super
|
376
|
-
change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
|
377
182
|
end
|
378
183
|
|
184
|
+
# https://materialize.com/docs/sql/alter-rename/
|
379
185
|
def change_column(table_name, column_name, type, options = {}) #:nodoc:
|
380
186
|
clear_cache!
|
381
187
|
sqls, procs = Array(change_column_for_alter(table_name, column_name, type, options)).partition { |v| v.is_a?(String) }
|
@@ -383,11 +189,12 @@ module ActiveRecord
|
|
383
189
|
procs.each(&:call)
|
384
190
|
end
|
385
191
|
|
386
|
-
#
|
192
|
+
# https://materialize.com/docs/sql/alter-rename/
|
387
193
|
def change_column_default(table_name, column_name, default_or_changes) # :nodoc:
|
388
194
|
execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_default_for_alter(table_name, column_name, default_or_changes)}"
|
389
195
|
end
|
390
196
|
|
197
|
+
# https://materialize.com/docs/sql/alter-rename/
|
391
198
|
def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
|
392
199
|
clear_cache!
|
393
200
|
unless null || default.nil?
|
@@ -397,21 +204,7 @@ module ActiveRecord
|
|
397
204
|
execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_null_for_alter(table_name, column_name, null, default)}"
|
398
205
|
end
|
399
206
|
|
400
|
-
#
|
401
|
-
def change_column_comment(table_name, column_name, comment_or_changes) # :nodoc:
|
402
|
-
clear_cache!
|
403
|
-
comment = extract_new_comment_value(comment_or_changes)
|
404
|
-
execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{quote_column_name(column_name)} IS #{quote(comment)}"
|
405
|
-
end
|
406
|
-
|
407
|
-
# Adds comment for given table or drops it if +comment+ is a +nil+
|
408
|
-
def change_table_comment(table_name, comment_or_changes) # :nodoc:
|
409
|
-
clear_cache!
|
410
|
-
comment = extract_new_comment_value(comment_or_changes)
|
411
|
-
execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS #{quote(comment)}"
|
412
|
-
end
|
413
|
-
|
414
|
-
# Renames a column in a table.
|
207
|
+
# https://materialize.com/docs/sql/alter-rename/
|
415
208
|
def rename_column(table_name, column_name, new_column_name) #:nodoc:
|
416
209
|
clear_cache!
|
417
210
|
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
|
@@ -420,9 +213,7 @@ module ActiveRecord
|
|
420
213
|
|
421
214
|
def add_index(table_name, column_name, options = {}) #:nodoc:
|
422
215
|
index_name, index_type, index_columns_and_opclasses, index_options, index_algorithm, index_using, comment = add_index_options(table_name, column_name, **options)
|
423
|
-
execute("CREATE
|
424
|
-
execute "COMMENT ON INDEX #{quote_column_name(index_name)} IS #{quote(comment)}" if comment
|
425
|
-
end
|
216
|
+
execute("CREATE INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns_and_opclasses})")
|
426
217
|
end
|
427
218
|
|
428
219
|
def remove_index(table_name, options = {}) #:nodoc:
|
@@ -457,78 +248,17 @@ module ActiveRecord
|
|
457
248
|
execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
|
458
249
|
end
|
459
250
|
|
460
|
-
def foreign_keys(table_name)
|
461
|
-
scope = quoted_scope(table_name)
|
462
|
-
fk_info = exec_query(<<~SQL, "SCHEMA")
|
463
|
-
SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid
|
464
|
-
FROM pg_constraint c
|
465
|
-
JOIN pg_class t1 ON c.conrelid = t1.oid
|
466
|
-
JOIN pg_class t2 ON c.confrelid = t2.oid
|
467
|
-
JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
|
468
|
-
JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
|
469
|
-
JOIN pg_namespace t3 ON c.connamespace = t3.oid
|
470
|
-
WHERE c.contype = 'f'
|
471
|
-
AND t1.relname = #{scope[:name]}
|
472
|
-
AND t3.nspname = #{scope[:schema]}
|
473
|
-
ORDER BY c.conname
|
474
|
-
SQL
|
475
|
-
|
476
|
-
fk_info.map do |row|
|
477
|
-
options = {
|
478
|
-
column: row["column"],
|
479
|
-
name: row["name"],
|
480
|
-
primary_key: row["primary_key"]
|
481
|
-
}
|
482
|
-
|
483
|
-
options[:on_delete] = extract_foreign_key_action(row["on_delete"])
|
484
|
-
options[:on_update] = extract_foreign_key_action(row["on_update"])
|
485
|
-
options[:validate] = row["valid"]
|
486
|
-
|
487
|
-
ForeignKeyDefinition.new(table_name, row["to_table"], options)
|
488
|
-
end
|
489
|
-
end
|
490
|
-
|
491
|
-
def foreign_tables
|
492
|
-
query_values(data_source_sql(type: "FOREIGN TABLE"), "SCHEMA")
|
493
|
-
end
|
494
|
-
|
495
|
-
def foreign_table_exists?(table_name)
|
496
|
-
query_values(data_source_sql(table_name, type: "FOREIGN TABLE"), "SCHEMA").any? if table_name.present?
|
497
|
-
end
|
498
|
-
|
499
251
|
# Maps logical Rails types to Materialize-specific data types.
|
500
|
-
def type_to_sql(type, limit: nil, precision: nil, scale: nil,
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
when nil, 0..0x3fffffff; super(type)
|
508
|
-
else raise ArgumentError, "No binary type has byte size #{limit}. The limit on binary can be at most 1GB - 1byte."
|
509
|
-
end
|
510
|
-
when "text"
|
511
|
-
# Materialize doesn't support limits on text columns.
|
512
|
-
# The hard limit is 1GB, according to section 8.3 in the manual.
|
513
|
-
case limit
|
514
|
-
when nil, 0..0x3fffffff; super(type)
|
515
|
-
else raise ArgumentError, "No text type has byte size #{limit}. The limit on text can be at most 1GB - 1byte."
|
516
|
-
end
|
517
|
-
when "integer"
|
518
|
-
case limit
|
519
|
-
when 1, 2; "smallint"
|
520
|
-
when nil, 3, 4; "integer"
|
521
|
-
when 5..8; "bigint"
|
522
|
-
else raise ArgumentError, "No integer type has byte size #{limit}. Use a numeric with scale 0 instead."
|
523
|
-
end
|
524
|
-
else
|
525
|
-
super
|
526
|
-
end
|
527
|
-
|
528
|
-
sql = "#{sql}[]" if array && type != :primary_key
|
529
|
-
sql
|
252
|
+
def type_to_sql(type, limit: nil, precision: nil, scale: nil, **) # :nodoc:
|
253
|
+
type = type.to_sym if type
|
254
|
+
if native = native_database_types[type]
|
255
|
+
(native.is_a?(Hash) ? native[:name] : native).dup
|
256
|
+
else
|
257
|
+
type.to_s
|
258
|
+
end
|
530
259
|
end
|
531
260
|
|
261
|
+
# TODO: verify same functionality as postgres
|
532
262
|
# Materialize requires the ORDER BY columns in the select list for distinct queries, and
|
533
263
|
# requires that the ORDER BY include the distinct column.
|
534
264
|
def columns_for_distinct(columns, orders) #:nodoc:
|
@@ -536,8 +266,9 @@ module ActiveRecord
|
|
536
266
|
# Convert Arel node to string
|
537
267
|
s = visitor.compile(s) unless s.is_a?(String)
|
538
268
|
# Remove any ASC/DESC modifiers
|
539
|
-
s
|
540
|
-
|
269
|
+
s
|
270
|
+
.gsub(/\s+(?:ASC|DESC)\b/i, "")
|
271
|
+
.gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
|
541
272
|
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
|
542
273
|
|
543
274
|
(order_columns << super).join(", ")
|
@@ -602,29 +333,22 @@ module ActiveRecord
|
|
602
333
|
end
|
603
334
|
|
604
335
|
def new_column_from_field(table_name, field)
|
605
|
-
column_name, type,
|
606
|
-
type_metadata = fetch_type_metadata(column_name, type
|
336
|
+
column_name, type, nullable, default, comment = field
|
337
|
+
type_metadata = fetch_type_metadata(column_name, type)
|
607
338
|
default_value = extract_value_from_default(default)
|
608
339
|
default_function = extract_default_function(default_value, default)
|
609
|
-
|
610
|
-
if match = default_function&.match(/\Anextval\('"?(?<sequence_name>.+_(?<suffix>seq\d*))"?'::regclass\)\z/)
|
611
|
-
serial = sequence_name_from_parts(table_name, column_name, match[:suffix]) == match[:sequence_name]
|
612
|
-
end
|
613
|
-
|
614
340
|
Materialize::Column.new(
|
615
341
|
column_name,
|
616
342
|
default_value,
|
617
343
|
type_metadata,
|
618
|
-
|
344
|
+
nullable,
|
619
345
|
default_function,
|
620
|
-
|
621
|
-
comment: comment.presence,
|
622
|
-
serial: serial
|
346
|
+
comment: comment.presence
|
623
347
|
)
|
624
348
|
end
|
625
349
|
|
626
|
-
def fetch_type_metadata(column_name, sql_type
|
627
|
-
cast_type =
|
350
|
+
def fetch_type_metadata(column_name, sql_type)
|
351
|
+
cast_type = lookup_cast_type(sql_type)
|
628
352
|
simple_type = SqlTypeMetadata.new(
|
629
353
|
sql_type: sql_type,
|
630
354
|
type: cast_type.type,
|
@@ -632,7 +356,7 @@ module ActiveRecord
|
|
632
356
|
precision: cast_type.precision,
|
633
357
|
scale: cast_type.scale,
|
634
358
|
)
|
635
|
-
Materialize::TypeMetadata.new(simple_type
|
359
|
+
Materialize::TypeMetadata.new(simple_type)
|
636
360
|
end
|
637
361
|
|
638
362
|
def sequence_name_from_parts(table_name, column_name, suffix)
|
@@ -705,12 +429,34 @@ module ActiveRecord
|
|
705
429
|
|
706
430
|
def data_source_sql(name = nil, type: nil)
|
707
431
|
scope = quoted_scope(name, type: type)
|
708
|
-
scope[:type] ||= "'
|
432
|
+
scope[:type] ||= "'table','view'"
|
433
|
+
sql = <<~SQL
|
434
|
+
SELECT
|
435
|
+
o.name
|
436
|
+
FROM mz_objects o
|
437
|
+
LEFT JOIN mz_schemas s ON o.schema_id = s.id
|
438
|
+
LEFT JOIN mz_databases d ON s.database_id = d.id
|
439
|
+
LEFT JOIN mz_sources src ON src.id = o.id
|
440
|
+
LEFT JOIN mz_indexes i ON i.on_id = o.id
|
441
|
+
WHERE
|
442
|
+
d.name = #{quote(current_database)}
|
443
|
+
AND s.name = #{scope[:schema]}
|
444
|
+
AND o.type IN (#{scope[:type]})
|
445
|
+
SQL
|
446
|
+
|
447
|
+
# Sources
|
448
|
+
sql += " AND src.id is NULL" unless scope[:type].include? 'source'
|
449
|
+
|
450
|
+
# Find with name
|
451
|
+
sql += " AND o.name = #{scope[:name]}" if scope[:name]
|
452
|
+
|
453
|
+
sql += " GROUP BY o.id, o.name"
|
454
|
+
if type == "MATERIALIZED"
|
455
|
+
sql += " HAVING COUNT(i.id) > 0"
|
456
|
+
elsif scope[:type].include?('view')
|
457
|
+
sql += " HAVING COUNT(i.id) = 0"
|
458
|
+
end
|
709
459
|
|
710
|
-
sql = +"SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace"
|
711
|
-
sql << " WHERE n.nspname = #{scope[:schema]}"
|
712
|
-
sql << " AND c.relname = #{scope[:name]}" if scope[:name]
|
713
|
-
sql << " AND c.relkind IN (#{scope[:type]})"
|
714
460
|
sql
|
715
461
|
end
|
716
462
|
|
@@ -719,11 +465,13 @@ module ActiveRecord
|
|
719
465
|
type = \
|
720
466
|
case type
|
721
467
|
when "BASE TABLE"
|
722
|
-
"'
|
468
|
+
"'table'"
|
469
|
+
when "MATERIALIZED"
|
470
|
+
"'view'"
|
471
|
+
when "SOURCE"
|
472
|
+
"'source'"
|
723
473
|
when "VIEW"
|
724
|
-
"'
|
725
|
-
when "FOREIGN TABLE"
|
726
|
-
"'f'"
|
474
|
+
"'view'"
|
727
475
|
end
|
728
476
|
scope = {}
|
729
477
|
scope[:schema] = schema ? quote(schema) : "ANY (current_schemas(false))"
|