activerecord6-redshift-adapter 1.1.2 → 1.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 5830bb316b6a3021508d6542b09478a0b0c3ce7a
4
- data.tar.gz: eb66565b485ab9464f93fe5f2e392a37a64d2805
2
+ SHA256:
3
+ metadata.gz: aed3175ea88bb9b6b163ffc7b198768b81c16330dc1d7b480d7d1a17356f6033
4
+ data.tar.gz: 66de7284e67e34a05bbd376be4cd8c3ee0a0c3b7565db16229a7c1512eb0de41
5
5
  SHA512:
6
- metadata.gz: 69d0323991a44986cef4b87d9268eaeff017c46dfd83cd6785753353c18900cc0dedde17539ef4c99e476d10f4b61e262aea755aaea8acae9070763d61f8aa79
7
- data.tar.gz: 819f6b941172c242926a3fc6229fe4b4d12742c89115178cfa4ae4d929e5eb2eea0cee773b852126074b2d26993db6e2faef0b99ecf6aafec75500bd0e49e41a
6
+ metadata.gz: c840dd282c228c3aab781e770ac40a8c5a2ee3ddb71c6d380f27cf6a9c411ed3ff5c659cfa6dbaa768da6db58dadbdefec8736309f4cff360a5639fcf2c83c96
7
+ data.tar.gz: bce5739dd4b207c1c92f726b80ad0a920eeb04b3f5d76800e5ff0121bd3a2c79474a967795c6ca4c7d567b6c8654dd9620188aa8397f0ee0a38886a072eb0439
@@ -47,9 +47,12 @@ module ActiveRecord
47
47
  def select_value(arel, name = nil, binds = [])
48
48
  # In Rails 5.2, arel_from_relation replaced binds_from_relation,
49
49
  # so we see which method exists to get the variables
50
+ #
51
+ # In Rails 6.0 to_sql_and_binds began only returning sql, with
52
+ # to_sql_and_binds serving as a replacement
50
53
  if respond_to?(:arel_from_relation, true)
51
54
  arel = arel_from_relation(arel)
52
- sql, binds = to_sql(arel, binds)
55
+ sql, binds = to_sql_and_binds(arel, binds)
53
56
  else
54
57
  arel, binds = binds_from_relation arel, binds
55
58
  sql = to_sql(arel, binds)
@@ -62,9 +65,12 @@ module ActiveRecord
62
65
  def select_values(arel, name = nil)
63
66
  # In Rails 5.2, arel_from_relation replaced binds_from_relation,
64
67
  # so we see which method exists to get the variables
68
+ #
69
+ # In Rails 6.0 to_sql_and_binds began only returning sql, with
70
+ # to_sql_and_binds serving as a replacement
65
71
  if respond_to?(:arel_from_relation, true)
66
72
  arel = arel_from_relation(arel)
67
- sql, binds = to_sql(arel, [])
73
+ sql, binds = to_sql_and_binds(arel, [])
68
74
  else
69
75
  arel, binds = binds_from_relation arel, []
70
76
  sql = to_sql(arel, binds)
@@ -54,8 +54,8 @@ module ActiveRecord
54
54
 
55
55
  private
56
56
 
57
- def create_column_definition(name, type)
58
- Redshift::ColumnDefinition.new name, type
57
+ def create_column_definition(*args)
58
+ Redshift::ColumnDefinition.new(*args)
59
59
  end
60
60
  end
61
61
 
@@ -5,7 +5,7 @@ module ActiveRecord
5
5
  private
6
6
 
7
7
  def visit_ColumnDefinition(o)
8
- o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale)
8
+ o.sql_type = type_to_sql(o.type, limit: o.limit, precision: o.precision, scale: o.scale)
9
9
  super
10
10
  end
11
11
 
@@ -277,11 +277,11 @@ module ActiveRecord
277
277
  def change_column(table_name, column_name, type, options = {})
278
278
  clear_cache!
279
279
  quoted_table_name = quote_table_name(table_name)
280
- sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale])
280
+ sql_type = type_to_sql(type, limit: options[:limit], precision: options[:precision], scale: options[:scale])
281
281
  sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}"
282
282
  sql << " USING #{options[:using]}" if options[:using]
283
283
  if options[:cast_as]
284
- sql << " USING CAST(#{quote_column_name(column_name)} AS #{type_to_sql(options[:cast_as], options[:limit], options[:precision], options[:scale])})"
284
+ sql << " USING CAST(#{quote_column_name(column_name)} AS #{type_to_sql(options[:cast_as], limit: options[:limit], precision: options[:precision], scale: options[:scale])})"
285
285
  end
286
286
  execute sql
287
287
 
@@ -372,7 +372,7 @@ module ActiveRecord
372
372
  end
373
373
 
374
374
  # Maps logical Rails types to PostgreSQL-specific data types.
375
- def type_to_sql(type, limit = nil, precision = nil, scale = nil)
375
+ def type_to_sql(type, limit: nil, precision: nil, scale: nil, **)
376
376
  case type.to_s
377
377
  when 'integer'
378
378
  return 'integer' unless limit
@@ -12,10 +12,14 @@ require 'active_record/connection_adapters/redshift/schema_statements'
12
12
  require 'active_record/connection_adapters/redshift/type_metadata'
13
13
  require 'active_record/connection_adapters/redshift/database_statements'
14
14
 
15
+ require 'active_record/tasks/database_tasks'
16
+
15
17
  require 'pg'
16
18
 
17
19
  require 'ipaddr'
18
20
 
21
+ ActiveRecord::Tasks::DatabaseTasks.register_task(/redshift/, "ActiveRecord::Tasks::PostgreSQLDatabaseTasks")
22
+
19
23
  module ActiveRecord
20
24
  module ConnectionHandling # :nodoc:
21
25
  RS_VALID_CONN_PARAMS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout,
@@ -78,10 +82,10 @@ module ActiveRecord
78
82
  string: { name: "varchar" },
79
83
  text: { name: "varchar" },
80
84
  integer: { name: "integer" },
81
- float: { name: "float" },
85
+ float: { name: "decimal" },
82
86
  decimal: { name: "decimal" },
83
87
  datetime: { name: "timestamp" },
84
- time: { name: "time" },
88
+ time: { name: "timestamp" },
85
89
  date: { name: "date" },
86
90
  bigint: { name: "bigint" },
87
91
  boolean: { name: "boolean" },
@@ -122,55 +126,29 @@ module ActiveRecord
122
126
  { concurrently: 'CONCURRENTLY' }
123
127
  end
124
128
 
125
- class StatementPool < ConnectionAdapters::StatementPool
129
+ class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
126
130
  def initialize(connection, max)
127
131
  super(max)
128
132
  @connection = connection
129
133
  @counter = 0
130
- @cache = Hash.new { |h,pid| h[pid] = {} }
131
134
  end
132
135
 
133
- def each(&block); cache.each(&block); end
134
- def key?(key); cache.key?(key); end
135
- def [](key); cache[key]; end
136
- def length; cache.length; end
137
-
138
136
  def next_key
139
137
  "a#{@counter + 1}"
140
138
  end
141
139
 
142
140
  def []=(sql, key)
143
- while @max <= cache.size
144
- dealloc(cache.shift.last)
145
- end
146
- @counter += 1
147
- cache[sql] = key
148
- end
149
-
150
- def clear
151
- cache.each_value do |stmt_key|
152
- dealloc stmt_key
153
- end
154
- cache.clear
155
- end
156
-
157
- def delete(sql_key)
158
- dealloc cache[sql_key]
159
- cache.delete sql_key
141
+ super.tap { @counter += 1 }
160
142
  end
161
143
 
162
144
  private
163
-
164
- def cache
165
- @cache[Process.pid]
166
- end
167
-
168
145
  def dealloc(key)
169
146
  @connection.query "DEALLOCATE #{key}" if connection_active?
147
+ rescue PG::Error
170
148
  end
171
149
 
172
150
  def connection_active?
173
- @connection.status == PG::Connection::CONNECTION_OK
151
+ @connection.status == PG::CONNECTION_OK
174
152
  rescue PG::Error
175
153
  false
176
154
  end
@@ -255,14 +233,6 @@ module ActiveRecord
255
233
  true
256
234
  end
257
235
 
258
- # Enable standard-conforming strings if available.
259
- def set_standard_conforming_strings
260
- old, self.client_min_messages = client_min_messages, 'panic'
261
- execute('SET standard_conforming_strings = on', 'SCHEMA') rescue nil
262
- ensure
263
- self.client_min_messages = old
264
- end
265
-
266
236
  def supports_ddl_transactions?
267
237
  true
268
238
  end
@@ -342,7 +312,7 @@ module ActiveRecord
342
312
  @connection.server_version
343
313
  end
344
314
 
345
- def translate_exception(exception, message)
315
+ def translate_exception(exception, message:, sql:, binds:)
346
316
  return exception unless exception.respond_to?(:result)
347
317
 
348
318
  case exception.message
@@ -496,39 +466,68 @@ module ActiveRecord
496
466
  ret
497
467
  end
498
468
 
469
+
499
470
  def exec_no_cache(sql, name, binds)
500
- log(sql, name, binds) { @connection.async_exec(sql, []) }
471
+ materialize_transactions
472
+
473
+ # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
474
+ # made since we established the connection
475
+ update_typemap_for_default_timezone
476
+
477
+ type_casted_binds = type_casted_binds(binds)
478
+ log(sql, name, binds, type_casted_binds) do
479
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
480
+ @connection.exec_params(sql, type_casted_binds)
481
+ end
482
+ end
501
483
  end
502
484
 
503
485
  def exec_cache(sql, name, binds)
504
- stmt_key = prepare_statement(sql)
505
- type_casted_binds = binds.map { |col, val|
506
- [col, type_cast(val, col)]
507
- }
486
+ materialize_transactions
487
+ update_typemap_for_default_timezone
508
488
 
509
- log(sql, name, type_casted_binds, stmt_key) do
510
- @connection.exec_prepared(stmt_key, type_casted_binds.map { |_, val| val })
489
+ stmt_key = prepare_statement(sql, binds)
490
+ type_casted_binds = type_casted_binds(binds)
491
+
492
+ log(sql, name, binds, type_casted_binds, stmt_key) do
493
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
494
+ @connection.exec_prepared(stmt_key, type_casted_binds)
495
+ end
511
496
  end
512
497
  rescue ActiveRecord::StatementInvalid => e
513
- pgerror = e.original_exception
514
-
515
- # Get the PG code for the failure. Annoyingly, the code for
516
- # prepared statements whose return value may have changed is
517
- # FEATURE_NOT_SUPPORTED. Check here for more details:
518
- # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
519
- begin
520
- code = pgerror.result.result_error_field(PG::Result::PG_DIAG_SQLSTATE)
521
- rescue
522
- raise e
523
- end
524
- if FEATURE_NOT_SUPPORTED == code
525
- @statements.delete sql_key(sql)
526
- retry
498
+ raise unless is_cached_plan_failure?(e)
499
+
500
+ # Nothing we can do if we are in a transaction because all commands
501
+ # will raise InFailedSQLTransaction
502
+ if in_transaction?
503
+ raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
527
504
  else
528
- raise e
505
+ @lock.synchronize do
506
+ # outside of transactions we can simply flush this query and retry
507
+ @statements.delete sql_key(sql)
508
+ end
509
+ retry
529
510
  end
530
511
  end
531
512
 
513
+ # Annoyingly, the code for prepared statements whose return value may
514
+ # have changed is FEATURE_NOT_SUPPORTED.
515
+ #
516
+ # This covers various different error types so we need to do additional
517
+ # work to classify the exception definitively as a
518
+ # ActiveRecord::PreparedStatementCacheExpired
519
+ #
520
+ # Check here for more details:
521
+ # https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
522
+ CACHED_PLAN_HEURISTIC = "cached plan must not change result type"
523
+ def is_cached_plan_failure?(e)
524
+ pgerror = e.cause
525
+ code = pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE)
526
+ code == FEATURE_NOT_SUPPORTED && pgerror.message.include?(CACHED_PLAN_HEURISTIC)
527
+ rescue
528
+ false
529
+ end
530
+
532
531
  # Returns the statement identifier for the client side cache
533
532
  # of statements
534
533
  def sql_key(sql)
@@ -537,34 +536,31 @@ module ActiveRecord
537
536
 
538
537
  # Prepare the statement if it hasn't been prepared, return
539
538
  # the statement key.
540
- def prepare_statement(sql)
541
- sql_key = sql_key(sql)
542
- unless @statements.key? sql_key
543
- nextkey = @statements.next_key
544
- begin
545
- @connection.prepare nextkey, sql
546
- rescue => e
547
- raise translate_exception_class(e, sql)
539
+ def prepare_statement(sql, binds)
540
+ @lock.synchronize do
541
+ sql_key = sql_key(sql)
542
+ unless @statements.key? sql_key
543
+ nextkey = @statements.next_key
544
+ begin
545
+ @connection.prepare nextkey, sql
546
+ rescue => e
547
+ raise translate_exception_class(e, sql, binds)
548
+ end
549
+ # Clear the queue
550
+ @connection.get_last_result
551
+ @statements[sql_key] = nextkey
548
552
  end
549
- # Clear the queue
550
- @connection.get_last_result
551
- @statements[sql_key] = nextkey
553
+ @statements[sql_key]
552
554
  end
553
- @statements[sql_key]
554
555
  end
555
556
 
556
557
  # Connects to a PostgreSQL server and sets up the adapter depending on the
557
558
  # connected server's characteristics.
558
559
  def connect
559
- @connection = PG::Connection.connect(@connection_parameters)
560
-
560
+ @connection = PG.connect(@connection_parameters)
561
561
  configure_connection
562
- rescue ::PG::Error => error
563
- if error.message.include?("does not exist")
564
- raise ActiveRecord::NoDatabaseError.new(error.message, error)
565
- else
566
- raise
567
- end
562
+ add_pg_encoders
563
+ add_pg_decoders
568
564
  end
569
565
 
570
566
  # Configures the encoding, verbosity, schema search path, and time zone of the connection.
@@ -575,17 +571,18 @@ module ActiveRecord
575
571
  end
576
572
  self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
577
573
 
578
- # SET statements from :variables config hash
579
- # http://www.postgresql.org/docs/8.3/static/sql-set.html
580
- variables = @config[:variables] || {}
581
- variables.map do |k, v|
582
- if v == ':default' || v == :default
583
- # Sets the value to the global or compile default
584
- execute("SET SESSION #{k} TO DEFAULT", 'SCHEMA')
585
- elsif !v.nil?
586
- execute("SET SESSION #{k} TO #{quote(v)}", 'SCHEMA')
574
+ variables = @config.fetch(:variables, {}).stringify_keys
575
+
576
+ # If using Active Record's time zone support configure the connection to return
577
+ # TIMESTAMP WITH ZONE types in UTC.
578
+ unless variables["timezone"]
579
+ if ActiveRecord::Base.default_timezone == :utc
580
+ variables["timezone"] = "UTC"
581
+ elsif @local_tz
582
+ variables["timezone"] = @local_tz
587
583
  end
588
584
  end
585
+
589
586
  end
590
587
 
591
588
  def last_insert_id_result(sequence_name) #:nodoc:
@@ -622,13 +619,111 @@ module ActiveRecord
622
619
  end_sql
623
620
  end
624
621
 
625
- def extract_table_ref_from_insert_sql(sql) # :nodoc:
622
+ def extract_table_ref_from_insert_sql(sql)
626
623
  sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im]
627
624
  $1.strip if $1
628
625
  end
629
626
 
627
+ def arel_visitor
628
+ Arel::Visitors::PostgreSQL.new(self)
629
+ end
630
+
631
+ def build_statement_pool
632
+ StatementPool.new(@connection, self.class.type_cast_config_to_integer(@config[:statement_limit]))
633
+ end
634
+
635
+
636
+ def can_perform_case_insensitive_comparison_for?(column)
637
+ @case_insensitive_cache ||= {}
638
+ @case_insensitive_cache[column.sql_type] ||= begin
639
+ sql = <<~SQL
640
+ SELECT exists(
641
+ SELECT * FROM pg_proc
642
+ WHERE proname = 'lower'
643
+ AND proargtypes = ARRAY[#{quote column.sql_type}::regtype]::oidvector
644
+ ) OR exists(
645
+ SELECT * FROM pg_proc
646
+ INNER JOIN pg_cast
647
+ ON ARRAY[casttarget]::oidvector = proargtypes
648
+ WHERE proname = 'lower'
649
+ AND castsource = #{quote column.sql_type}::regtype
650
+ )
651
+ SQL
652
+ execute_and_clear(sql, "SCHEMA", []) do |result|
653
+ result.getvalue(0, 0)
654
+ end
655
+ end
656
+ end
657
+
658
+ def add_pg_encoders
659
+ map = PG::TypeMapByClass.new
660
+ map[Integer] = PG::TextEncoder::Integer.new
661
+ map[TrueClass] = PG::TextEncoder::Boolean.new
662
+ map[FalseClass] = PG::TextEncoder::Boolean.new
663
+ @connection.type_map_for_queries = map
664
+ end
665
+
666
+ def update_typemap_for_default_timezone
667
+ if @default_timezone != ActiveRecord::Base.default_timezone && @timestamp_decoder
668
+ decoder_class = ActiveRecord::Base.default_timezone == :utc ?
669
+ PG::TextDecoder::TimestampUtc :
670
+ PG::TextDecoder::TimestampWithoutTimeZone
671
+
672
+ @timestamp_decoder = decoder_class.new(@timestamp_decoder.to_h)
673
+ @connection.type_map_for_results.add_coder(@timestamp_decoder)
674
+ @default_timezone = ActiveRecord::Base.default_timezone
675
+ end
676
+ end
677
+
678
+
679
+ def add_pg_decoders
680
+ @default_timezone = nil
681
+ @timestamp_decoder = nil
682
+
683
+ coders_by_name = {
684
+ "int2" => PG::TextDecoder::Integer,
685
+ "int4" => PG::TextDecoder::Integer,
686
+ "int8" => PG::TextDecoder::Integer,
687
+ "oid" => PG::TextDecoder::Integer,
688
+ "float4" => PG::TextDecoder::Float,
689
+ "float8" => PG::TextDecoder::Float,
690
+ "bool" => PG::TextDecoder::Boolean,
691
+ }
692
+
693
+ if defined?(PG::TextDecoder::TimestampUtc)
694
+ # Use native PG encoders available since pg-1.1
695
+ coders_by_name["timestamp"] = PG::TextDecoder::TimestampUtc
696
+ coders_by_name["timestamptz"] = PG::TextDecoder::TimestampWithTimeZone
697
+ end
698
+
699
+ known_coder_types = coders_by_name.keys.map { |n| quote(n) }
700
+ query = <<~SQL % known_coder_types.join(", ")
701
+ SELECT t.oid, t.typname
702
+ FROM pg_type as t
703
+ WHERE t.typname IN (%s)
704
+ SQL
705
+ coders = execute_and_clear(query, "SCHEMA", []) do |result|
706
+ result
707
+ .map { |row| construct_coder(row, coders_by_name[row["typname"]]) }
708
+ .compact
709
+ end
710
+
711
+ map = PG::TypeMapByOid.new
712
+ coders.each { |coder| map.add_coder(coder) }
713
+ @connection.type_map_for_results = map
714
+
715
+ # extract timestamp decoder for use in update_typemap_for_default_timezone
716
+ @timestamp_decoder = coders.find { |coder| coder.name == "timestamp" }
717
+ update_typemap_for_default_timezone
718
+ end
719
+
720
+ def construct_coder(row, coder_class)
721
+ return unless coder_class
722
+ coder_class.new(oid: row["oid"].to_i, name: row["typname"])
723
+ end
724
+
630
725
  def create_table_definition(*args) # :nodoc:
631
- Redshift::TableDefinition.new(*args)
726
+ Redshift::TableDefinition.new(self, *args)
632
727
  end
633
728
  end
634
729
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord6-redshift-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nancy Foen
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2019-09-05 00:00:00.000000000 Z
14
+ date: 2020-08-04 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: pg
@@ -31,22 +31,22 @@ dependencies:
31
31
  name: activerecord
32
32
  requirement: !ruby/object:Gem::Requirement
33
33
  requirements:
34
- - - "~>"
35
- - !ruby/object:Gem::Version
36
- version: '6.0'
37
34
  - - ">="
38
35
  - !ruby/object:Gem::Version
39
36
  version: 6.0.0
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '6.0'
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - "~>"
45
- - !ruby/object:Gem::Version
46
- version: '6.0'
47
44
  - - ">="
48
45
  - !ruby/object:Gem::Version
49
46
  version: 6.0.0
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '6.0'
50
50
  description: Amazon Redshift _makeshift_ adapter for ActiveRecord 6.
51
51
  email: fantast.d@gmail.com
52
52
  executables: []
@@ -91,8 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
91
  - !ruby/object:Gem::Version
92
92
  version: '0'
93
93
  requirements: []
94
- rubyforge_project:
95
- rubygems_version: 2.5.2.3
94
+ rubygems_version: 3.0.3
96
95
  signing_key:
97
96
  specification_version: 4
98
97
  summary: Amazon Redshift adapter for ActiveRecord