activerecord6-redshift-adapter 1.1.2 → 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
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