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.
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))"