activerecord8-redshift-adapter 1.0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fa5caefae917701fe9b5f6b84c36d7f42d7c17fcd24ad6416345a66585f7b208
4
+ data.tar.gz: 4349e6d9fca32e14ee7fe6e042c607daead2207923b4c5c22e53bf65ca52947d
5
+ SHA512:
6
+ metadata.gz: 531ce0f10c5ab65e7e6528b5de60923b9e36b1e558ef1c239076f40633529660c5dc98fc21691abdc0e360424b4af46f5ebae9952f6ea7be1601fe8f2166cec3
7
+ data.tar.gz: e4da7dfd607589a38de261d19e1198695ce91b0b4303c96fa85cdf78846bad3de4bb3f67c8268594bc3f398de35debfeae72752df375baacbc9087a4c8921d94
data/LICENSE ADDED
@@ -0,0 +1,55 @@
1
+ -----------------------------------------------------------------------------------
2
+ The MIT License (MIT)
3
+
4
+ Copyright (c) 2026 Nicholas Guarino (ActiveRecord 8.1 compatibility updates)
5
+ Copyright (c) 2004-2013 David Heinemeier Hansson (original code author)
6
+ Copyright (c) 2013 Minero Aoki
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
9
+ this software and associated documentation files (the "Software"), to deal in
10
+ the Software without restriction, including without limitation the rights to
11
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
12
+ the Software, and to permit persons to whom the Software is furnished to do so,
13
+ subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
20
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
21
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
22
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+
25
+ -----------------------------------------------------------------------------------
26
+ Copyright (c) 2010-2013, Fiksu, Inc.
27
+ All rights reserved.
28
+
29
+ Redistribution and use in source and binary forms, with or without
30
+ modification, are permitted provided that the following conditions are
31
+ met:
32
+
33
+ o Redistributions of source code must retain the above copyright
34
+ notice, this list of conditions and the following disclaimer.
35
+
36
+ o Redistributions in binary form must reproduce the above copyright
37
+ notice, this list of conditions and the following disclaimer in the
38
+ documentation and/or other materials provided with the
39
+ distribution.
40
+
41
+ o Fiksu, Inc. nor the names of its contributors may be used to
42
+ endorse or promote products derived from this software without
43
+ specific prior written permission.
44
+
45
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
46
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
47
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
48
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
49
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
50
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
51
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
52
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
53
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
54
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
55
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,168 @@
1
+ activerecord8-redshift-adapter
2
+ ==============================
3
+
4
+ [![Test](https://github.com/nguarino522/activerecord8-redshift-adapter/actions/workflows/test.yml/badge.svg)](https://github.com/nguarino522/activerecord8-redshift-adapter/actions/workflows/test.yml)
5
+ [![Gem Version](https://badge.fury.io/rb/activerecord8-redshift-adapter.svg)](https://rubygems.org/gems/activerecord8-redshift-adapter)
6
+ [![License](https://img.shields.io/badge/license-MIT%20%26%20BSD--3--Clause-blue)](LICENSE)
7
+
8
+ Amazon Redshift adapter for ActiveRecord 8.1 (Rails 8.1).
9
+
10
+ Forked from [charitywater/activerecord-redshift-adapter](https://github.com/charitywater/activerecord-redshift-adapter) (which itself descends from the original [fiksu/activerecord-redshift-adapter](https://github.com/fiksu/activerecord-redshift-adapter)). The prior forks targeted Rails 8.0; this gem updates the adapter to work with the changes introduced in ActiveRecord 8.1. Thanks to all the prior authors.
11
+
12
+ Installation
13
+ ------------
14
+
15
+ Add to your Gemfile:
16
+
17
+ ```ruby
18
+ gem 'activerecord8-redshift-adapter', '~> 1.0'
19
+ ```
20
+
21
+ Then:
22
+
23
+ ```bash
24
+ bundle install
25
+ ```
26
+
27
+ Or install it directly:
28
+
29
+ ```bash
30
+ gem install activerecord8-redshift-adapter
31
+ ```
32
+
33
+ Usage
34
+ -----
35
+
36
+ In `config/database.yml`:
37
+
38
+ ```yaml
39
+ development:
40
+ adapter: redshift
41
+ host: host
42
+ port: port
43
+ database: db
44
+ username: user
45
+ password: password
46
+ encoding: utf8
47
+ ```
48
+
49
+ Or via a connection URL:
50
+
51
+ ```ruby
52
+ class SomeModel < ApplicationRecord
53
+ establish_connection('redshift://username:password@host/database')
54
+ end
55
+ ```
56
+
57
+ ### Typical pattern: Redshift as a secondary read-only connection
58
+
59
+ Most apps use Redshift alongside a primary OLTP database (Postgres, MySQL). With ActiveRecord multi-database support:
60
+
61
+ ```yaml
62
+ production:
63
+ primary:
64
+ adapter: postgresql
65
+ database: app_production
66
+ # ...
67
+ warehouse:
68
+ adapter: redshift
69
+ host: my-cluster.xxxxxx.us-east-1.redshift.amazonaws.com
70
+ port: 5439
71
+ database: analytics
72
+ username: app_reader
73
+ password: <%= ENV["REDSHIFT_PASSWORD"] %>
74
+ replica: true
75
+ ```
76
+
77
+ ```ruby
78
+ class AnalyticsRecord < ApplicationRecord
79
+ self.abstract_class = true
80
+ connects_to database: { reading: :warehouse }
81
+ end
82
+
83
+ class PageView < AnalyticsRecord
84
+ self.table_name = "page_views"
85
+ end
86
+ ```
87
+
88
+ Compatibility
89
+ -------------
90
+
91
+ - Ruby `>= 3.2` (tested on 3.2, 3.3, 3.4)
92
+ - ActiveRecord `>= 8.1, < 9.0` (Rails 8.1)
93
+ - `pg` `~> 1.0`
94
+
95
+ For older Rails versions, use the matching gem in the same family:
96
+ [`activerecord7-redshift-adapter`](https://rubygems.org/gems/activerecord7-redshift-adapter),
97
+ [`activerecord6-redshift-adapter`](https://rubygems.org/gems/activerecord6-redshift-adapter),
98
+ [`activerecord5-redshift-adapter`](https://rubygems.org/gems/activerecord5-redshift-adapter).
99
+ For Rails 8.0, the upstream [`activerecord-redshift-adapter`](https://rubygems.org/gems/activerecord-redshift-adapter) `8.0.x` still works.
100
+
101
+ Development
102
+ -----------
103
+
104
+ ```bash
105
+ bundle install
106
+ gem build activerecord8-redshift-adapter.gemspec
107
+ ```
108
+
109
+ To release a new version (maintainers), bump `s.version` in the gemspec, tag the commit, then:
110
+
111
+ ```bash
112
+ gem push activerecord8-redshift-adapter-<version>.gem
113
+ ```
114
+
115
+ Testing
116
+ -------
117
+
118
+ The test suite runs against a local **PostgreSQL** database (Redshift is wire-compatible with Postgres 8.0, so most adapter behavior can be exercised locally). For Redshift-specific behavior — `RETURNING`, identity columns, distribution keys, system catalogs — run the smoke script against a real cluster (see below).
119
+
120
+ ### Prerequisites
121
+
122
+ - PostgreSQL running locally (any reasonably modern version)
123
+ - A database the test user can connect to and create tables in
124
+
125
+ ```bash
126
+ createdb redshift_adapter_test
127
+ ```
128
+
129
+ ### Running the suite
130
+
131
+ ```bash
132
+ bundle install
133
+ bundle exec rake test
134
+ ```
135
+
136
+ Connection settings come from standard `PG*` environment variables, with sensible defaults:
137
+
138
+ | Variable | Default |
139
+ | ------------ | ----------------------- |
140
+ | `PGHOST` | `localhost` |
141
+ | `PGPORT` | `5432` |
142
+ | `PGDATABASE` | `redshift_adapter_test` |
143
+ | `PGUSER` | `$USER` |
144
+ | `PGPASSWORD` | _(unset)_ |
145
+
146
+ Override any of them inline, e.g.:
147
+
148
+ ```bash
149
+ PGUSER=postgres PGPASSWORD=secret bundle exec rake test
150
+ ```
151
+
152
+ ### Smoke-testing against real Redshift
153
+
154
+ `test/smoke.rb` is a small ad-hoc script (not part of the rake task) that connects to a real Redshift cluster using the same env vars and exercises basic queries. Use it to validate the adapter against your actual cluster before deploying:
155
+
156
+ ```bash
157
+ PGHOST=my-cluster.xxxxxx.us-east-1.redshift.amazonaws.com \
158
+ PGPORT=5439 \
159
+ PGDATABASE=analytics \
160
+ PGUSER=myuser \
161
+ PGPASSWORD=... \
162
+ bundle exec ruby test/smoke.rb
163
+ ```
164
+
165
+ License
166
+ -------
167
+
168
+ Dual-licensed under MIT (Rails-derived code) and BSD-3-Clause (Fiksu Redshift-specific portions). See [`LICENSE`](LICENSE) for full text.
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Redshift
6
+ module ArrayParser # :nodoc:
7
+ DOUBLE_QUOTE = '"'
8
+ BACKSLASH = '\\'
9
+ COMMA = ','
10
+ BRACKET_OPEN = '{'
11
+ BRACKET_CLOSE = '}'
12
+
13
+ def parse_pg_array(string) # :nodoc:
14
+ local_index = 0
15
+ array = []
16
+ while local_index < string.length
17
+ case string[local_index]
18
+ when BRACKET_OPEN
19
+ local_index, array = parse_array_contents(array, string, local_index + 1)
20
+ when BRACKET_CLOSE
21
+ return array
22
+ end
23
+ local_index += 1
24
+ end
25
+
26
+ array
27
+ end
28
+
29
+ private
30
+
31
+ def parse_array_contents(array, string, index)
32
+ is_escaping = false
33
+ is_quoted = false
34
+ was_quoted = false
35
+ current_item = ''
36
+
37
+ local_index = index
38
+ while local_index
39
+ token = string[local_index]
40
+ if is_escaping
41
+ current_item << token
42
+ is_escaping = false
43
+ elsif is_quoted
44
+ case token
45
+ when DOUBLE_QUOTE
46
+ is_quoted = false
47
+ was_quoted = true
48
+ when BACKSLASH
49
+ is_escaping = true
50
+ else
51
+ current_item << token
52
+ end
53
+ else
54
+ case token
55
+ when BACKSLASH
56
+ is_escaping = true
57
+ when COMMA
58
+ add_item_to_array(array, current_item, was_quoted)
59
+ current_item = ''
60
+ was_quoted = false
61
+ when DOUBLE_QUOTE
62
+ is_quoted = true
63
+ when BRACKET_OPEN
64
+ internal_items = []
65
+ local_index, internal_items = parse_array_contents(internal_items, string, local_index + 1)
66
+ array.push(internal_items)
67
+ when BRACKET_CLOSE
68
+ add_item_to_array(array, current_item, was_quoted)
69
+ return local_index, array
70
+ else
71
+ current_item << token
72
+ end
73
+ end
74
+
75
+ local_index += 1
76
+ end
77
+ [local_index, array]
78
+ end
79
+
80
+ def add_item_to_array(array, current_item, quoted)
81
+ return if !quoted && current_item.empty?
82
+
83
+ if !quoted && current_item == 'NULL'
84
+ array.push nil
85
+ else
86
+ array.push current_item
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ class RedshiftColumn < Column # :nodoc:
6
+ delegate :oid, :fmod, to: :sql_type_metadata
7
+
8
+ # Required for Rails 6.1, see https://github.com/rails/rails/pull/41756
9
+ mattr_reader :array, default: false
10
+ alias array? array
11
+
12
+ def initialize(name, cast_type, default, sql_type_metadata = nil, null = true, default_function = nil, **)
13
+ super
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Redshift
6
+ module DatabaseStatements
7
+ def explain(arel, binds = [])
8
+ sql = "EXPLAIN #{to_sql(arel, binds)}"
9
+ ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
10
+ end
11
+
12
+ class ExplainPrettyPrinter # :nodoc:
13
+ # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
14
+ # PostgreSQL shell:
15
+ #
16
+ # QUERY PLAN
17
+ # ------------------------------------------------------------------------------
18
+ # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0)
19
+ # Join Filter: (posts.user_id = users.id)
20
+ # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4)
21
+ # Index Cond: (id = 1)
22
+ # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4)
23
+ # Filter: (posts.user_id = 1)
24
+ # (6 rows)
25
+ #
26
+ def pp(result)
27
+ header = result.columns.first
28
+ lines = result.rows.map(&:first)
29
+
30
+ # We add 2 because there's one char of padding at both sides, note
31
+ # the extra hyphens in the example above.
32
+ width = [header, *lines].map(&:length).max + 2
33
+
34
+ pp = []
35
+
36
+ pp << header.center(width).rstrip
37
+ pp << '-' * width
38
+
39
+ pp += lines.map { |line| " #{line}" }
40
+
41
+ nrows = result.rows.length
42
+ rows_label = nrows == 1 ? 'row' : 'rows'
43
+ pp << "(#{nrows} #{rows_label})"
44
+
45
+ "#{pp.join("\n")}\n"
46
+ end
47
+ end
48
+
49
+ def select_value(arel, name = nil, binds = [])
50
+ # In Rails 5.2, arel_from_relation replaced binds_from_relation,
51
+ # so we see which method exists to get the variables
52
+ #
53
+ # In Rails 6.0 to_sql_and_binds began only returning sql, with
54
+ # to_sql_and_binds serving as a replacement
55
+ if respond_to?(:arel_from_relation, true)
56
+ arel = arel_from_relation(arel)
57
+ sql, binds = to_sql_and_binds(arel, binds)
58
+ else
59
+ arel, binds = binds_from_relation arel, binds
60
+ sql = to_sql(arel, binds)
61
+ end
62
+ execute_and_clear(sql, name, binds) do |result|
63
+ result.getvalue(0, 0) if result.ntuples > 0 && result.nfields > 0
64
+ end
65
+ end
66
+
67
+ def select_values(arel, name = nil)
68
+ # In Rails 5.2, arel_from_relation replaced binds_from_relation,
69
+ # so we see which method exists to get the variables
70
+ #
71
+ # In Rails 6.0 to_sql_and_binds began only returning sql, with
72
+ # to_sql_and_binds serving as a replacement
73
+ if respond_to?(:arel_from_relation, true)
74
+ arel = arel_from_relation(arel)
75
+ sql, binds = to_sql_and_binds(arel, [])
76
+ else
77
+ arel, binds = binds_from_relation arel, []
78
+ sql = to_sql(arel, binds)
79
+ end
80
+
81
+ execute_and_clear(sql, name, binds) do |result|
82
+ if result.nfields > 0
83
+ result.column_values(0)
84
+ else
85
+ []
86
+ end
87
+ end
88
+ end
89
+
90
+ # Executes a SELECT query and returns an array of rows. Each row is an
91
+ # array of field values.
92
+ def select_rows(arel, name = nil, binds = [])
93
+ if respond_to?(:arel_from_relation, true)
94
+ arel = arel_from_relation(arel)
95
+ sql, binds = to_sql_and_binds(arel, [])
96
+ else
97
+ arel, binds = binds_from_relation arel, []
98
+ sql = to_sql(arel, binds)
99
+ end
100
+ execute_and_clear(sql, name, binds, &:values)
101
+ end
102
+
103
+ # The internal PostgreSQL identifier of the money data type.
104
+ MONEY_COLUMN_TYPE_OID = 790 # :nodoc:
105
+ # The internal PostgreSQL identifier of the BYTEA data type.
106
+ BYTEA_COLUMN_TYPE_OID = 17 # :nodoc:
107
+
108
+ # create a 2D array representing the result set
109
+ def result_as_array(res) # :nodoc:
110
+ # check if we have any binary column and if they need escaping
111
+ ftypes = Array.new(res.nfields) do |i|
112
+ [i, res.ftype(i)]
113
+ end
114
+
115
+ rows = res.values
116
+ return rows unless ftypes.any? do |_, x|
117
+ [BYTEA_COLUMN_TYPE_OID, MONEY_COLUMN_TYPE_OID].include?(x)
118
+ end
119
+
120
+ typehash = ftypes.group_by { |_, type| type }
121
+ binaries = typehash[BYTEA_COLUMN_TYPE_OID] || []
122
+ monies = typehash[MONEY_COLUMN_TYPE_OID] || []
123
+
124
+ rows.each do |row|
125
+ # unescape string passed BYTEA field (OID == 17)
126
+ binaries.each do |index, _|
127
+ row[index] = unescape_bytea(row[index])
128
+ end
129
+
130
+ # If this is a money type column and there are any currency symbols,
131
+ # then strip them off. Indeed it would be prettier to do this in
132
+ # PostgreSQLColumn.string_to_decimal but would break form input
133
+ # fields that call value_before_type_cast.
134
+ monies.each do |index, _|
135
+ data = row[index]
136
+ # Because money output is formatted according to the locale, there are two
137
+ # cases to consider (note the decimal separators):
138
+ # (1) $12,345,678.12
139
+ # (2) $12.345.678,12
140
+ case data
141
+ when /^-?\D+[\d,]+\.\d{2}$/ # (1)
142
+ data.gsub!(/[^-\d.]/, '')
143
+ when /^-?\D+[\d.]+,\d{2}$/ # (2)
144
+ data.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ # Queries the database and returns the results in an Array-like object
151
+ def query(sql, name = nil) # :nodoc:
152
+ log(sql, name) do
153
+ result_as_array @connection.async_exec(sql)
154
+ end
155
+ end
156
+
157
+ # Executes an SQL statement, returning a PG::Result object on success
158
+ # or raising a PG::Error exception otherwise.
159
+ def execute(sql, name = nil)
160
+ log(sql, name) do
161
+ @connection.async_exec(sql)
162
+ end
163
+ end
164
+
165
+ def exec_query(sql, name = 'SQL', binds = [], prepare: false)
166
+ execute_and_clear(sql, name, binds, prepare: prepare) do |result|
167
+ types = {}
168
+ fields = result.fields
169
+ fields.each_with_index do |fname, i|
170
+ ftype = result.ftype i
171
+ fmod = result.fmod i
172
+ types[fname] = get_oid_type(ftype, fmod, fname)
173
+ end
174
+ ActiveRecord::Result.new(fields, result.values, types)
175
+ end
176
+ end
177
+
178
+ def exec_delete(sql, name = 'SQL', binds = [])
179
+ execute_and_clear(sql, name, binds, &:cmd_tuples)
180
+ end
181
+ alias exec_update exec_delete
182
+
183
+ def sql_for_insert(sql, pk, id_value, sequence_name, binds)
184
+ if pk.nil?
185
+ # Extract the table from the insert sql. Yuck.
186
+ table_ref = extract_table_ref_from_insert_sql(sql)
187
+ pk = primary_key(table_ref) if table_ref
188
+ end
189
+
190
+ sql = "#{sql} RETURNING #{quote_column_name(pk)}" if pk && use_insert_returning?
191
+
192
+ super
193
+ end
194
+
195
+ def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning: nil)
196
+ val = exec_query(sql, name, binds)
197
+ if !use_insert_returning? && pk
198
+ unless sequence_name
199
+ table_ref = extract_table_ref_from_insert_sql(sql)
200
+ sequence_name = default_sequence_name(table_ref, pk)
201
+ return val unless sequence_name
202
+ end
203
+ last_insert_id_result(sequence_name)
204
+ else
205
+ val
206
+ end
207
+ end
208
+
209
+ def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true) # :nodoc:
210
+ execute_and_clear(sql, name, binds, prepare: prepare, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |result|
211
+ types = {}
212
+ fields = result.fields
213
+ fields.each_with_index do |fname, i|
214
+ ftype = result.ftype i
215
+ fmod = result.fmod i
216
+ types[fname] = types[i] = get_oid_type(ftype, fmod, fname)
217
+ end
218
+ build_result(columns: fields, rows: result.values, column_types: types)
219
+ end
220
+ end
221
+
222
+ # Begins a transaction.
223
+ def begin_db_transaction
224
+ execute 'BEGIN'
225
+ end
226
+
227
+ def begin_isolated_db_transaction(isolation)
228
+ begin_db_transaction
229
+ execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
230
+ end
231
+
232
+ # Commits a transaction.
233
+ def commit_db_transaction
234
+ execute 'COMMIT'
235
+ end
236
+
237
+ # Aborts a transaction.
238
+ def exec_rollback_db_transaction
239
+ execute 'ROLLBACK'
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Redshift
6
+ module OID # :nodoc:
7
+ class DateTime < Type::DateTime # :nodoc:
8
+ def type_cast_for_database(value)
9
+ if has_precision? && value.acts_like?(:time) && value.year <= 0
10
+ bce_year = format('%04d', -value.year + 1)
11
+ "#{super.sub(/^-?\d+/, bce_year)} BC"
12
+ else
13
+ super
14
+ end
15
+ end
16
+
17
+ def cast_value(value)
18
+ if value.is_a?(::String)
19
+ case value
20
+ when 'infinity' then ::Float::INFINITY
21
+ when '-infinity' then -::Float::INFINITY
22
+ when / BC$/
23
+ astronomical_year = format('%04d', -value[/^\d+/].to_i + 1)
24
+ super(value.sub(/ BC$/, '').sub(/^\d+/, astronomical_year))
25
+ else
26
+ super
27
+ end
28
+ else
29
+ value
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Redshift
6
+ module OID # :nodoc:
7
+ class Decimal < Type::Decimal # :nodoc:
8
+ def infinity(**options)
9
+ BigDecimal('Infinity') * (options[:negative] ? -1 : 1)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Redshift
6
+ module OID # :nodoc:
7
+ class Json < Type::Value # :nodoc:
8
+ include ActiveModel::Type::Helpers::Mutable
9
+
10
+ def type
11
+ :json
12
+ end
13
+
14
+ def type_cast_from_database(value)
15
+ if value.is_a?(::String)
16
+ begin
17
+ ::ActiveSupport::JSON.decode(value)
18
+ rescue StandardError
19
+ nil
20
+ end
21
+ else
22
+ super
23
+ end
24
+ end
25
+
26
+ def type_cast_for_database(value)
27
+ if value.is_a?(::Array) || value.is_a?(::Hash)
28
+ ::ActiveSupport::JSON.encode(value)
29
+ else
30
+ super
31
+ end
32
+ end
33
+
34
+ def accessor
35
+ ActiveRecord::Store::StringKeyedHashAccessor
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end