activerecord7-redshift-adapter-pennylane 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Redshift
6
+ module Quoting
7
+ # Escapes binary strings for bytea input to the database.
8
+ def escape_bytea(value)
9
+ @connection.escape_bytea(value) if value
10
+ end
11
+
12
+ # Unescapes bytea output from a database to the binary string it represents.
13
+ # NOTE: This is NOT an inverse of escape_bytea! This is only to be used
14
+ # on escaped binary output from database drive.
15
+ def unescape_bytea(value)
16
+ @connection.unescape_bytea(value) if value
17
+ end
18
+
19
+ # Quotes strings for use in SQL input.
20
+ def quote_string(s) # :nodoc:
21
+ @connection.escape(s)
22
+ end
23
+
24
+ # Checks the following cases:
25
+ #
26
+ # - table_name
27
+ # - "table.name"
28
+ # - schema_name.table_name
29
+ # - schema_name."table.name"
30
+ # - "schema.name".table_name
31
+ # - "schema.name"."table.name"
32
+ def quote_table_name(name)
33
+ Utils.extract_schema_qualified_name(name.to_s).quoted
34
+ end
35
+
36
+ def quote_table_name_for_assignment(_table, attr)
37
+ quote_column_name(attr)
38
+ end
39
+
40
+ # Quotes column names for use in SQL queries.
41
+ def quote_column_name(name) # :nodoc:
42
+ PG::Connection.quote_ident(name.to_s)
43
+ end
44
+
45
+ # Quotes schema names for use in SQL queries.
46
+ def quote_schema_name(name)
47
+ PG::Connection.quote_ident(name)
48
+ end
49
+
50
+ # Quote date/time values for use in SQL input.
51
+ def quoted_date(value) # :nodoc:
52
+ result = super
53
+
54
+ if value.year <= 0
55
+ bce_year = format('%04d', -value.year + 1)
56
+ result = "#{result.sub(/^-?\d+/, bce_year)} BC"
57
+ end
58
+ result
59
+ end
60
+
61
+ # Does not quote function default values for UUID columns
62
+ def quote_default_value(value, column) # :nodoc:
63
+ if column.type == :uuid && value =~ /\(\)/
64
+ value
65
+ else
66
+ quote(value, column)
67
+ end
68
+ end
69
+
70
+ def quote(value)
71
+ case value
72
+ when Type::Binary::Data
73
+ "'#{escape_bytea(value.to_s)}'"
74
+ when Float
75
+ if value.infinite? || value.nan?
76
+ "'#{value}'"
77
+ else
78
+ super
79
+ end
80
+ else
81
+ super
82
+ end
83
+ end
84
+
85
+ def type_cast(value)
86
+ case value
87
+ when Type::Binary::Data
88
+ # Return a bind param hash with format as binary.
89
+ # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc
90
+ # for more information
91
+ { value: value.to_s, format: 1 }
92
+ else
93
+ super
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Redshift
6
+ module ReferentialIntegrity # :nodoc:
7
+ def supports_disable_referential_integrity? # :nodoc:
8
+ true
9
+ end
10
+
11
+ def disable_referential_integrity # :nodoc:
12
+ yield
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Redshift
6
+ module ColumnMethods
7
+ # Defines the primary key field.
8
+ # Use of the native PostgreSQL UUID type is supported, and can be used
9
+ # by defining your tables as such:
10
+ #
11
+ # create_table :stuffs, id: :uuid do |t|
12
+ # t.string :content
13
+ # t.timestamps
14
+ # end
15
+ #
16
+ # By default, this will use the +uuid_generate_v4()+ function from the
17
+ # +uuid-ossp+ extension, which MUST be enabled on your database. To enable
18
+ # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your
19
+ # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can
20
+ # set the +:default+ option to +nil+:
21
+ #
22
+ # create_table :stuffs, id: false do |t|
23
+ # t.primary_key :id, :uuid, default: nil
24
+ # t.uuid :foo_id
25
+ # t.timestamps
26
+ # end
27
+ #
28
+ # You may also pass a different UUID generation function from +uuid-ossp+
29
+ # or another library.
30
+ #
31
+ # Note that setting the UUID primary key default value to +nil+ will
32
+ # require you to assure that you always provide a UUID value before saving
33
+ # a record (as primary keys cannot be +nil+). This might be done via the
34
+ # +SecureRandom.uuid+ method and a +before_save+ callback, for instance.
35
+ def primary_key(name, type = :primary_key, **options)
36
+ return super unless type == :uuid
37
+
38
+ options[:default] = options.fetch(:default, 'uuid_generate_v4()')
39
+ options[:primary_key] = true
40
+ column name, type, options
41
+ end
42
+
43
+ def json(name, **options)
44
+ column(name, :json, options)
45
+ end
46
+
47
+ def jsonb(name, **options)
48
+ column(name, :jsonb, options)
49
+ end
50
+ end
51
+
52
+ class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
53
+ end
54
+
55
+ class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
56
+ include ColumnMethods
57
+
58
+ private
59
+
60
+ def create_column_definition(*args)
61
+ Redshift::ColumnDefinition.new(*args)
62
+ end
63
+ end
64
+
65
+ class Table < ActiveRecord::ConnectionAdapters::Table
66
+ include ColumnMethods
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Redshift
6
+ module ColumnDumper
7
+ # Adds +:array+ option to the default set provided by the
8
+ # AbstractAdapter
9
+ def prepare_column_options(column) # :nodoc:
10
+ spec = super
11
+ spec[:default] = "\"#{column.default_function}\"" if column.default_function
12
+ spec
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,423 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Redshift
6
+ class SchemaCreation < SchemaCreation
7
+ private
8
+
9
+ def visit_ColumnDefinition(o)
10
+ o.sql_type = type_to_sql(o.type, limit: o.limit, precision: o.precision, scale: o.scale)
11
+ super
12
+ end
13
+
14
+ def add_column_options!(sql, options)
15
+ column = options.fetch(:column) { return super }
16
+ if column.type == :uuid && options[:default] =~ /\(\)/
17
+ sql << " DEFAULT #{options[:default]}"
18
+ else
19
+ super
20
+ end
21
+ end
22
+ end
23
+
24
+ module SchemaStatements
25
+ # Drops the database specified on the +name+ attribute
26
+ # and creates it again using the provided +options+.
27
+ def recreate_database(name, **options) # :nodoc:
28
+ drop_database(name)
29
+ create_database(name, options)
30
+ end
31
+
32
+ # Create a new Redshift database. Options include <tt>:owner</tt>, <tt>:template</tt>,
33
+ # <tt>:encoding</tt> (defaults to utf8), <tt>:collation</tt>, <tt>:ctype</tt>,
34
+ # <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
35
+ # <tt>:charset</tt> while Redshift uses <tt>:encoding</tt>).
36
+ #
37
+ # Example:
38
+ # create_database config[:database], config
39
+ # create_database 'foo_development', encoding: 'unicode'
40
+ def create_database(name, **options)
41
+ options = { encoding: 'utf8' }.merge!(options.symbolize_keys)
42
+
43
+ option_string = options.inject('') do |memo, (key, value)|
44
+ next memo unless key == :owner
45
+
46
+ memo + " OWNER = \"#{value}\""
47
+ end
48
+
49
+ execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}"
50
+ end
51
+
52
+ # Drops a Redshift database.
53
+ #
54
+ # Example:
55
+ # drop_database 'matt_development'
56
+ def drop_database(name) # :nodoc:
57
+ execute "DROP DATABASE #{quote_table_name(name)}"
58
+ end
59
+
60
+ # Returns the list of all tables in the schema search path or a specified schema.
61
+ def tables(name = nil)
62
+ if name
63
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
64
+ Passing arguments to #tables is deprecated without replacement.
65
+ MSG
66
+ end
67
+
68
+ select_values('SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))', 'SCHEMA')
69
+ end
70
+
71
+ # :nodoc
72
+ def data_sources
73
+ select_values(<<-SQL, 'SCHEMA')
74
+ SELECT c.relname
75
+ FROM pg_class c
76
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
77
+ WHERE c.relkind IN ('r', 'v','m') -- (r)elation/table, (v)iew, (m)aterialized view
78
+ AND n.nspname = ANY (current_schemas(false))
79
+ SQL
80
+ end
81
+
82
+ # Returns true if table exists.
83
+ # If the schema is not specified as part of +name+ then it will only find tables within
84
+ # the current schema search path (regardless of permissions to access tables in other schemas)
85
+ def table_exists?(name)
86
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
87
+ #table_exists? currently checks both tables and views.
88
+ This behavior is deprecated and will be changed with Rails 5.1 to only check tables.
89
+ Use #data_source_exists? instead.
90
+ MSG
91
+
92
+ data_source_exists?(name)
93
+ end
94
+
95
+ def data_source_exists?(name)
96
+ name = Utils.extract_schema_qualified_name(name.to_s)
97
+ return false unless name.identifier
98
+
99
+ select_value(<<-SQL, 'SCHEMA').to_i > 0
100
+ SELECT COUNT(*)
101
+ FROM pg_class c
102
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
103
+ WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view
104
+ AND c.relname = '#{name.identifier}'
105
+ AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'}
106
+ SQL
107
+ end
108
+
109
+ def views # :nodoc:
110
+ select_values(<<-SQL, 'SCHEMA')
111
+ SELECT c.relname
112
+ FROM pg_class c
113
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
114
+ WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
115
+ AND n.nspname = ANY (current_schemas(false))
116
+ SQL
117
+ end
118
+
119
+ def view_exists?(view_name) # :nodoc:
120
+ name = Utils.extract_schema_qualified_name(view_name.to_s)
121
+ return false unless name.identifier
122
+
123
+ select_values(<<-SQL, 'SCHEMA').any?
124
+ SELECT c.relname
125
+ FROM pg_class c
126
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
127
+ WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
128
+ AND c.relname = '#{name.identifier}'
129
+ AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'}
130
+ SQL
131
+ end
132
+
133
+ def drop_table(table_name, **options)
134
+ execute "DROP TABLE #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
135
+ end
136
+
137
+ # Returns true if schema exists.
138
+ def schema_exists?(name)
139
+ select_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = '#{name}'", 'SCHEMA').to_i > 0
140
+ end
141
+
142
+ def index_name_exists?(_table_name, _index_name, _default)
143
+ false
144
+ end
145
+
146
+ # Returns an array of indexes for the given table.
147
+ def indexes(_table_name, _name = nil)
148
+ []
149
+ end
150
+
151
+ # Returns the list of all column definitions for a table.
152
+ def columns(table_name)
153
+ column_definitions(table_name.to_s).map do |column_name, type, default, notnull, oid, fmod|
154
+ default_value = extract_value_from_default(default)
155
+ type_metadata = fetch_type_metadata(column_name, type, oid, fmod)
156
+ default_function = extract_default_function(default_value, default)
157
+ new_column(column_name, default_value, type_metadata, notnull == 'f', table_name, default_function)
158
+ end
159
+ end
160
+
161
+ def new_column(name, default, sql_type_metadata = nil, null = true, _table_name = nil, default_function = nil) # :nodoc:
162
+ RedshiftColumn.new(name, default, sql_type_metadata, null, default_function)
163
+ end
164
+
165
+ # Returns the current database name.
166
+ def current_database
167
+ select_value('select current_database()', 'SCHEMA')
168
+ end
169
+
170
+ # Returns the current schema name.
171
+ def current_schema
172
+ select_value('SELECT current_schema', 'SCHEMA')
173
+ end
174
+
175
+ # Returns the current database encoding format.
176
+ def encoding
177
+ select_value(
178
+ "SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname LIKE '#{current_database}'", 'SCHEMA'
179
+ )
180
+ end
181
+
182
+ def collation; end
183
+
184
+ def ctype; end
185
+
186
+ # Returns an array of schema names.
187
+ def schema_names
188
+ select_value(<<-SQL, 'SCHEMA')
189
+ SELECT nspname
190
+ FROM pg_namespace
191
+ WHERE nspname !~ '^pg_.*'
192
+ AND nspname NOT IN ('information_schema')
193
+ ORDER by nspname;
194
+ SQL
195
+ end
196
+
197
+ # Creates a schema for the given schema name.
198
+ def create_schema(schema_name)
199
+ execute "CREATE SCHEMA #{quote_schema_name(schema_name)}"
200
+ end
201
+
202
+ # Drops the schema for the given schema name.
203
+ def drop_schema(schema_name, **options)
204
+ execute "DROP SCHEMA#{' IF EXISTS' if options[:if_exists]} #{quote_schema_name(schema_name)} CASCADE"
205
+ end
206
+
207
+ # Sets the schema search path to a string of comma-separated schema names.
208
+ # Names beginning with $ have to be quoted (e.g. $user => '$user').
209
+ # See: http://www.postgresql.org/docs/current/static/ddl-schemas.html
210
+ #
211
+ # This should be not be called manually but set in database.yml.
212
+ def schema_search_path=(schema_csv)
213
+ return unless schema_csv
214
+
215
+ execute("SET search_path TO #{schema_csv}", 'SCHEMA')
216
+ @schema_search_path = schema_csv
217
+ end
218
+
219
+ # Returns the active schema search path.
220
+ def schema_search_path
221
+ @schema_search_path ||= select_value('SHOW search_path', 'SCHEMA')
222
+ end
223
+
224
+ # Returns the sequence name for a table's primary key or some other specified key.
225
+ def default_sequence_name(table_name, pk = nil) # :nodoc:
226
+ result = serial_sequence(table_name, pk || 'id')
227
+ return nil unless result
228
+
229
+ Utils.extract_schema_qualified_name(result).to_s
230
+ rescue ActiveRecord::StatementInvalid
231
+ Redshift::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq").to_s
232
+ end
233
+
234
+ def serial_sequence(table, column)
235
+ select_value("SELECT pg_get_serial_sequence('#{table}', '#{column}')", 'SCHEMA')
236
+ end
237
+
238
+ def set_pk_sequence!(table, value); end
239
+
240
+ def reset_pk_sequence!(table, pk = nil, sequence = nil); end
241
+
242
+ def pk_and_sequence_for(_table) # :nodoc:
243
+ [nil, nil]
244
+ end
245
+
246
+ # Returns just a table's primary key
247
+ def primary_keys(table)
248
+ pks = query(<<-END_SQL, 'SCHEMA')
249
+ SELECT DISTINCT attr.attname
250
+ FROM pg_attribute attr
251
+ INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid
252
+ INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey)
253
+ WHERE cons.contype = 'p'
254
+ AND dep.refobjid = '#{quote_table_name(table)}'::regclass
255
+ END_SQL
256
+ pks.present? ? pks[0] : pks
257
+ end
258
+
259
+ # Renames a table.
260
+ # Also renames a table's primary key sequence if the sequence name exists and
261
+ # matches the Active Record default.
262
+ #
263
+ # Example:
264
+ # rename_table('octopuses', 'octopi')
265
+ def rename_table(table_name, new_name)
266
+ clear_cache!
267
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
268
+ end
269
+
270
+ def add_column(table_name, column_name, type, **options) # :nodoc:
271
+ clear_cache!
272
+ super
273
+ end
274
+
275
+ # Changes the column of a table.
276
+ def change_column(table_name, column_name, type, **options)
277
+ clear_cache!
278
+ quoted_table_name = quote_table_name(table_name)
279
+ sql_type = type_to_sql(type, limit: options[:limit], precision: options[:precision], scale: options[:scale])
280
+ sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}"
281
+ sql << " USING #{options[:using]}" if options[:using]
282
+ if options[:cast_as]
283
+ sql << " USING CAST(#{quote_column_name(column_name)} AS #{type_to_sql(options[:cast_as],
284
+ limit: options[:limit], precision: options[:precision], scale: options[:scale])})"
285
+ end
286
+ execute sql
287
+
288
+ change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
289
+ change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
290
+ end
291
+
292
+ # Changes the default value of a table column.
293
+ def change_column_default(table_name, column_name, default_or_changes)
294
+ clear_cache!
295
+ column = column_for(table_name, column_name)
296
+ return unless column
297
+
298
+ default = extract_new_default_value(default_or_changes)
299
+ alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s"
300
+ if default.nil?
301
+ # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will
302
+ # cast the default to the columns type, which leaves us with a default like "default NULL::character varying".
303
+ execute alter_column_query % 'DROP DEFAULT'
304
+ else
305
+ execute alter_column_query % "SET DEFAULT #{quote_default_value(default, column)}"
306
+ end
307
+ end
308
+
309
+ def change_column_null(table_name, column_name, null, default = nil)
310
+ clear_cache!
311
+ unless null || default.nil?
312
+ column = column_for(table_name, column_name)
313
+ if column
314
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_value(
315
+ default, column
316
+ )} WHERE #{quote_column_name(column_name)} IS NULL")
317
+ end
318
+ end
319
+ execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
320
+ end
321
+
322
+ # Renames a column in a table.
323
+ def rename_column(table_name, column_name, new_column_name) # :nodoc:
324
+ clear_cache!
325
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
326
+ end
327
+
328
+ def add_index(table_name, column_name, **options); end
329
+
330
+ def remove_index!(table_name, index_name); end
331
+
332
+ def rename_index(table_name, old_name, new_name); end
333
+
334
+ def foreign_keys(table_name)
335
+ fk_info = select_all(<<-SQL.strip_heredoc, 'SCHEMA')
336
+ SELECT t2.relname 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
337
+ FROM pg_constraint c
338
+ JOIN pg_class t1 ON c.conrelid = t1.oid
339
+ JOIN pg_class t2 ON c.confrelid = t2.oid
340
+ JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
341
+ JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
342
+ JOIN pg_namespace t3 ON c.connamespace = t3.oid
343
+ WHERE c.contype = 'f'
344
+ AND t1.relname = #{quote(table_name)}
345
+ AND t3.nspname = ANY (current_schemas(false))
346
+ ORDER BY c.conname
347
+ SQL
348
+
349
+ fk_info.map do |row|
350
+ options = {
351
+ column: row['column'],
352
+ name: row['name'],
353
+ primary_key: row['primary_key']
354
+ }
355
+
356
+ options[:on_delete] = extract_foreign_key_action(row['on_delete'])
357
+ options[:on_update] = extract_foreign_key_action(row['on_update'])
358
+
359
+ ForeignKeyDefinition.new(table_name, row['to_table'], options)
360
+ end
361
+ end
362
+
363
+ FOREIGN_KEY_ACTIONS = {
364
+ 'c' => :cascade,
365
+ 'n' => :nullify,
366
+ 'r' => :restrict
367
+ }.freeze
368
+
369
+ def extract_foreign_key_action(specifier)
370
+ FOREIGN_KEY_ACTIONS[specifier]
371
+ end
372
+
373
+ def index_name_length
374
+ 63
375
+ end
376
+
377
+ # Maps logical Rails types to PostgreSQL-specific data types.
378
+ def type_to_sql(type, limit: nil, precision: nil, scale: nil, **)
379
+ case type.to_s
380
+ when 'integer'
381
+ return 'integer' unless limit
382
+
383
+ case limit
384
+ when 1, 2 then 'smallint'
385
+ when nil, 3, 4 then 'integer'
386
+ when 5..8 then 'bigint'
387
+ else raise(ActiveRecordError,
388
+ "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
389
+ end
390
+ else
391
+ super
392
+ end
393
+ end
394
+
395
+ # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
396
+ # requires that the ORDER BY include the distinct column.
397
+ def columns_for_distinct(columns, orders) # :nodoc:
398
+ order_columns = orders.compact_blank.map { |s|
399
+ # Convert Arel node to string
400
+ s = visitor.compile(s) unless s.is_a?(String)
401
+ # Remove any ASC/DESC modifiers
402
+ s.gsub(/\s+(?:ASC|DESC)\b/i, "")
403
+ .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
404
+ }.compact_blank.map.with_index { |column, i| "#{column} AS alias_#{i}" }
405
+
406
+ (order_columns << super).join(", ")
407
+ end
408
+
409
+ def fetch_type_metadata(column_name, sql_type, oid, fmod)
410
+ cast_type = get_oid_type(oid.to_i, fmod.to_i, column_name, sql_type)
411
+ simple_type = SqlTypeMetadata.new(
412
+ sql_type: sql_type,
413
+ type: cast_type.type,
414
+ limit: cast_type.limit,
415
+ precision: cast_type.precision,
416
+ scale: cast_type.scale
417
+ )
418
+ TypeMetadata.new(simple_type, oid: oid, fmod: fmod)
419
+ end
420
+ end
421
+ end
422
+ end
423
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Redshift
6
+ class TypeMetadata < DelegateClass(SqlTypeMetadata)
7
+ attr_reader :oid, :fmod, :array
8
+
9
+ def initialize(type_metadata, oid: nil, fmod: nil)
10
+ super(type_metadata)
11
+ @type_metadata = type_metadata
12
+ @oid = oid
13
+ @fmod = fmod
14
+ @array = /\[\]$/ === type_metadata.sql_type
15
+ end
16
+
17
+ def sql_type
18
+ super.gsub(/\[\]$/, '')
19
+ end
20
+
21
+ def ==(other)
22
+ other.is_a?(Redshift::TypeMetadata) &&
23
+ attributes_for_hash == other.attributes_for_hash
24
+ end
25
+ alias eql? ==
26
+
27
+ def hash
28
+ attributes_for_hash.hash
29
+ end
30
+
31
+ protected
32
+
33
+ def attributes_for_hash
34
+ [self.class, @type_metadata, oid, fmod]
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end