activerecord-materialize-adapter 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (23) hide show
  1. checksums.yaml +4 -4
  2. data/lib/active_record/connection_adapters/materialize/column.rb +0 -9
  3. data/lib/active_record/connection_adapters/materialize/database_statements.rb +22 -41
  4. data/lib/active_record/connection_adapters/materialize/oid.rb +0 -12
  5. data/lib/active_record/connection_adapters/materialize/oid/type_map_initializer.rb +5 -48
  6. data/lib/active_record/connection_adapters/materialize/quoting.rb +1 -7
  7. data/lib/active_record/connection_adapters/materialize/schema/source_statements.rb +66 -0
  8. data/lib/active_record/connection_adapters/materialize/schema/view_statements.rb +22 -0
  9. data/lib/active_record/connection_adapters/materialize/schema_creation.rb +18 -53
  10. data/lib/active_record/connection_adapters/materialize/schema_definitions.rb +3 -6
  11. data/lib/active_record/connection_adapters/materialize/schema_dumper.rb +82 -22
  12. data/lib/active_record/connection_adapters/materialize/schema_statements.rb +102 -354
  13. data/lib/active_record/connection_adapters/materialize/version.rb +1 -1
  14. data/lib/active_record/connection_adapters/materialize_adapter.rb +100 -253
  15. metadata +9 -15
  16. data/lib/active_record/connection_adapters/materialize/oid/cidr.rb +0 -50
  17. data/lib/active_record/connection_adapters/materialize/oid/legacy_point.rb +0 -44
  18. data/lib/active_record/connection_adapters/materialize/oid/money.rb +0 -41
  19. data/lib/active_record/connection_adapters/materialize/oid/point.rb +0 -64
  20. data/lib/active_record/connection_adapters/materialize/oid/range.rb +0 -96
  21. data/lib/active_record/connection_adapters/materialize/oid/uuid.rb +0 -25
  22. data/lib/active_record/connection_adapters/materialize/oid/vector.rb +0 -28
  23. 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 :bigserial, :bit, :bit_varying, :cidr, :citext, :daterange,
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
- :bigserial
194
+ :bigint
198
195
  else
199
- :serial
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
- private
8
- def extensions(stream)
9
- extensions = @connection.extensions
10
- if extensions.any?
11
- stream.puts " # These are extensions that must be enabled in order to support this database"
12
- extensions.sort.each do |extension|
13
- stream.puts " enable_extension #{extension.inspect}"
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
- stream.puts
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) == :bigserial
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
- :bigserial
101
+ :bigint
38
102
  else
39
- :serial
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
- query_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = #{quote(name)}", "SCHEMA").to_i > 0
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
- table = quoted_scope(table_name)
49
- index = quoted_scope(index_name)
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
- scope = quoted_scope(table_name)
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
- result.map do |row|
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
- orders: orders,
123
- opclasses: opclasses,
124
- where: where,
125
- using: using.to_sym,
126
- comment: comment.presence
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
- # Returns the current database name.
98
+ # Unsupported current_database() use default config
153
99
  def current_database
154
- query_value("SELECT current_database()", "SCHEMA")
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
- # Returns the current database encoding format.
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()", "SCHEMA")
110
+ query_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = #{quote(current_database)}", "SCHEMA")
165
111
  end
166
112
 
167
- # Returns the current database collation.
113
+ # Unsupported collation
168
114
  def collation
169
- query_value("SELECT datcollate FROM pg_database WHERE datname = current_database()", "SCHEMA")
115
+ nil
170
116
  end
171
117
 
172
- # Returns the current database ctype.
118
+ # Get ctype
173
119
  def ctype
174
- query_value("SELECT datctype FROM pg_database WHERE datname = current_database()", "SCHEMA")
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 nspname
181
- FROM pg_namespace
182
- WHERE nspname !~ '^pg_.*'
183
- AND nspname NOT IN ('information_schema')
184
- ORDER by nspname;
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 current client message level.
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
- def serial_sequence(table, column)
235
- query_value("SELECT pg_get_serial_sequence(#{quote(table)}, #{quote(column)})", "SCHEMA")
236
- end
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 result.nil? || result.empty?
305
- result = query(<<~SQL, "SCHEMA")[0]
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
- [pk, nil]
164
+ []
330
165
  end
331
- rescue
332
- nil
333
166
  end
334
167
 
335
- def primary_keys(table_name) # :nodoc:
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
- pk, seq = pk_and_sequence_for(new_name)
361
- if pk
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
- # Changes the default value of a table column.
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
- # Adds comment for given table column or drops it if +comment+ is a +nil+
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 #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns_and_opclasses})#{index_options}").tap do
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, array: nil, **) # :nodoc:
501
- sql = \
502
- case type.to_s
503
- when "binary"
504
- # Materialize doesn't support limits on binary (bytea) columns.
505
- # The hard limit is 1GB, because of a 32-bit size field, and TOAST.
506
- case limit
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.gsub(/\s+(?:ASC|DESC)\b/i, "")
540
- .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
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, default, notnull, oid, fmod, collation, comment = field
606
- type_metadata = fetch_type_metadata(column_name, type, oid.to_i, fmod.to_i)
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
- !notnull,
344
+ nullable,
619
345
  default_function,
620
- collation: collation,
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, oid, fmod)
627
- cast_type = get_oid_type(oid, fmod, column_name, sql_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, oid: oid, fmod: fmod)
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] ||= "'r','v','m','p','f'" # (r)elation/table, (v)iew, (m)aterialized view, (p)artitioned table, (f)oreign table
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
- "'r','p'"
468
+ "'table'"
469
+ when "MATERIALIZED"
470
+ "'view'"
471
+ when "SOURCE"
472
+ "'source'"
723
473
  when "VIEW"
724
- "'v','m'"
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))"