activerecord-materialize-adapter 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/lib/active_record/connection_adapters/materialize/column.rb +30 -0
  4. data/lib/active_record/connection_adapters/materialize/database_statements.rb +199 -0
  5. data/lib/active_record/connection_adapters/materialize/explain_pretty_printer.rb +44 -0
  6. data/lib/active_record/connection_adapters/materialize/oid/array.rb +91 -0
  7. data/lib/active_record/connection_adapters/materialize/oid/bit.rb +53 -0
  8. data/lib/active_record/connection_adapters/materialize/oid/bit_varying.rb +15 -0
  9. data/lib/active_record/connection_adapters/materialize/oid/bytea.rb +17 -0
  10. data/lib/active_record/connection_adapters/materialize/oid/cidr.rb +50 -0
  11. data/lib/active_record/connection_adapters/materialize/oid/date.rb +23 -0
  12. data/lib/active_record/connection_adapters/materialize/oid/date_time.rb +23 -0
  13. data/lib/active_record/connection_adapters/materialize/oid/decimal.rb +15 -0
  14. data/lib/active_record/connection_adapters/materialize/oid/enum.rb +20 -0
  15. data/lib/active_record/connection_adapters/materialize/oid/hstore.rb +70 -0
  16. data/lib/active_record/connection_adapters/materialize/oid/inet.rb +15 -0
  17. data/lib/active_record/connection_adapters/materialize/oid/jsonb.rb +15 -0
  18. data/lib/active_record/connection_adapters/materialize/oid/legacy_point.rb +44 -0
  19. data/lib/active_record/connection_adapters/materialize/oid/money.rb +41 -0
  20. data/lib/active_record/connection_adapters/materialize/oid/oid.rb +15 -0
  21. data/lib/active_record/connection_adapters/materialize/oid/point.rb +64 -0
  22. data/lib/active_record/connection_adapters/materialize/oid/range.rb +96 -0
  23. data/lib/active_record/connection_adapters/materialize/oid/specialized_string.rb +18 -0
  24. data/lib/active_record/connection_adapters/materialize/oid/type_map_initializer.rb +112 -0
  25. data/lib/active_record/connection_adapters/materialize/oid/uuid.rb +25 -0
  26. data/lib/active_record/connection_adapters/materialize/oid/vector.rb +28 -0
  27. data/lib/active_record/connection_adapters/materialize/oid/xml.rb +30 -0
  28. data/lib/active_record/connection_adapters/materialize/oid.rb +35 -0
  29. data/lib/active_record/connection_adapters/materialize/quoting.rb +205 -0
  30. data/lib/active_record/connection_adapters/materialize/referential_integrity.rb +43 -0
  31. data/lib/active_record/connection_adapters/materialize/schema_creation.rb +76 -0
  32. data/lib/active_record/connection_adapters/materialize/schema_definitions.rb +222 -0
  33. data/lib/active_record/connection_adapters/materialize/schema_dumper.rb +49 -0
  34. data/lib/active_record/connection_adapters/materialize/schema_statements.rb +742 -0
  35. data/lib/active_record/connection_adapters/materialize/type_metadata.rb +36 -0
  36. data/lib/active_record/connection_adapters/materialize/utils.rb +80 -0
  37. data/lib/active_record/connection_adapters/materialize/version.rb +9 -0
  38. data/lib/active_record/connection_adapters/materialize_adapter.rb +952 -0
  39. data/lib/active_record/tasks/materialize_database_tasks.rb +130 -0
  40. data/lib/activerecord-materialize-adapter.rb +3 -0
  41. data/lib/materialize/errors/database_error.rb +10 -0
  42. data/lib/materialize/errors/incomplete_input.rb +10 -0
  43. metadata +170 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e1003f4f98f3745e7b60ae1784af47747ad13dcaa0dbc2dfc8fb96489e6a90f4
4
+ data.tar.gz: 07bfb83d66eea02e86e053a0be1ea88fb18a7a101a4ac89047889a098f2246d0
5
+ SHA512:
6
+ metadata.gz: 61b3e6efea873654b7a8225aed6062c3d87f8b78be411801e3fa9b25ed4a826bcce5a441c95caadf6ec4eb9d8792708e0e1cea56589e5e206c49d82b6eea15ff
7
+ data.tar.gz: 55bb91241a81f26ac8a3d2e662962c4195ba2517494fda6cd59549be030a3ee6d72137e7da933d463574cd314e88d66eeffd2445546b1cdf5f79cd4fafd3c42c
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2015 Henry Tseng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
22
+
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Materialize
6
+ class Column < ActiveRecord::ConnectionAdapters::Column # :nodoc:
7
+ delegate :oid, :fmod, to: :sql_type_metadata
8
+
9
+ def initialize(*, serial: nil, **)
10
+ super
11
+ @serial = serial
12
+ end
13
+
14
+ def serial?
15
+ @serial
16
+ end
17
+
18
+ def array
19
+ sql_type_metadata.sql_type.end_with?("[]")
20
+ end
21
+ alias :array? :array
22
+
23
+ def sql_type
24
+ super.sub(/\[\]\z/, "")
25
+ end
26
+ end
27
+ end
28
+ MaterializeColumn = Materialize::Column # :nodoc:
29
+ end
30
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'materialize/errors/incomplete_input'
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ module Materialize
8
+ module DatabaseStatements
9
+ def explain(arel, binds = [])
10
+ sql = "EXPLAIN #{to_sql(arel, binds)}"
11
+ Materialize::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", binds))
12
+ end
13
+
14
+ # The internal Materialize identifier of the money data type.
15
+ MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
16
+ # The internal Materialize identifier of the BYTEA data type.
17
+ BYTEA_COLUMN_TYPE_OID = 17 #:nodoc:
18
+
19
+ # create a 2D array representing the result set
20
+ def result_as_array(res) #:nodoc:
21
+ # check if we have any binary column and if they need escaping
22
+ ftypes = Array.new(res.nfields) do |i|
23
+ [i, res.ftype(i)]
24
+ end
25
+
26
+ rows = res.values
27
+ return rows unless ftypes.any? { |_, x|
28
+ x == BYTEA_COLUMN_TYPE_OID || x == MONEY_COLUMN_TYPE_OID
29
+ }
30
+
31
+ typehash = ftypes.group_by { |_, type| type }
32
+ binaries = typehash[BYTEA_COLUMN_TYPE_OID] || []
33
+ monies = typehash[MONEY_COLUMN_TYPE_OID] || []
34
+
35
+ rows.each do |row|
36
+ # unescape string passed BYTEA field (OID == 17)
37
+ binaries.each do |index, _|
38
+ row[index] = unescape_bytea(row[index])
39
+ end
40
+
41
+ # If this is a money type column and there are any currency symbols,
42
+ # then strip them off. Indeed it would be prettier to do this in
43
+ # MaterializeColumn.string_to_decimal but would break form input
44
+ # fields that call value_before_type_cast.
45
+ monies.each do |index, _|
46
+ data = row[index]
47
+ # Because money output is formatted according to the locale, there are two
48
+ # cases to consider (note the decimal separators):
49
+ # (1) $12,345,678.12
50
+ # (2) $12.345.678,12
51
+ case data
52
+ when /^-?\D+[\d,]+\.\d{2}$/ # (1)
53
+ data.gsub!(/[^-\d.]/, "")
54
+ when /^-?\D+[\d.]+,\d{2}$/ # (2)
55
+ data.gsub!(/[^-\d,]/, "").sub!(/,/, ".")
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ # Queries the database and returns the results in an Array-like object
62
+ def query(sql, name = nil) #:nodoc:
63
+ materialize_transactions
64
+
65
+ log(sql, name) do
66
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
67
+ result_as_array @connection.async_exec(sql)
68
+ end
69
+ end
70
+ end
71
+
72
+ READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(
73
+ :begin, :commit, :explain, :select, :set, :show, :release, :savepoint, :rollback, :with
74
+ ) # :nodoc:
75
+ private_constant :READ_QUERY
76
+
77
+ def write_query?(sql) # :nodoc:
78
+ !READ_QUERY.match?(sql)
79
+ end
80
+
81
+ # Executes an SQL statement, returning a PG::Result object on success
82
+ # or raising a PG::Error exception otherwise.
83
+ # Note: the PG::Result object is manually memory managed; if you don't
84
+ # need it specifically, you may want consider the <tt>exec_query</tt> wrapper.
85
+ def execute(sql, name = nil)
86
+ if preventing_writes? && write_query?(sql)
87
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
88
+ end
89
+
90
+ materialize_transactions
91
+
92
+ log(sql, name) do
93
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
94
+ @connection.async_exec(sql)
95
+ end
96
+ end
97
+
98
+ # Known issue: PG::InternalError: ERROR: At least one input has no complete timestamps yet
99
+ # https://github.com/MaterializeInc/materialize/issues/2917
100
+ rescue ActiveRecord::StatementInvalid => error
101
+ if error.message.include? "At least one input has no complete timestamps yet"
102
+ raise ::Materialize::Errors::IncompleteInput, error.message
103
+ else
104
+ raise
105
+ end
106
+ end
107
+
108
+ def exec_query(sql, name = "SQL", binds = [], prepare: false)
109
+ execute_and_clear(sql, name, binds, prepare: prepare) do |result|
110
+ types = {}
111
+ fields = result.fields
112
+ fields.each_with_index do |fname, i|
113
+ ftype = result.ftype i
114
+ fmod = result.fmod i
115
+ types[fname] = get_oid_type(ftype, fmod, fname)
116
+ end
117
+ ActiveRecord::Result.new(fields, result.values, types)
118
+ end
119
+ end
120
+
121
+ def exec_delete(sql, name = nil, binds = [])
122
+ execute_and_clear(sql, name, binds) { |result| result.cmd_tuples }
123
+ end
124
+ alias :exec_update :exec_delete
125
+
126
+ def sql_for_insert(sql, pk, binds) # :nodoc:
127
+ if pk.nil?
128
+ # Extract the table from the insert sql. Yuck.
129
+ table_ref = extract_table_ref_from_insert_sql(sql)
130
+ pk = primary_key(table_ref) if table_ref
131
+ end
132
+
133
+ if pk = suppress_composite_primary_key(pk)
134
+ sql = "#{sql} RETURNING #{quote_column_name(pk)}"
135
+ end
136
+
137
+ super
138
+ end
139
+ private :sql_for_insert
140
+
141
+ def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil)
142
+ if use_insert_returning? || pk == false
143
+ super
144
+ else
145
+ result = exec_query(sql, name, binds)
146
+ unless sequence_name
147
+ table_ref = extract_table_ref_from_insert_sql(sql)
148
+ if table_ref
149
+ pk = primary_key(table_ref) if pk.nil?
150
+ pk = suppress_composite_primary_key(pk)
151
+ sequence_name = default_sequence_name(table_ref, pk)
152
+ end
153
+ return result unless sequence_name
154
+ end
155
+ last_insert_id_result(sequence_name)
156
+ end
157
+ end
158
+
159
+ # Begins a transaction.
160
+ def begin_db_transaction
161
+ execute "BEGIN"
162
+ end
163
+
164
+ def begin_isolated_db_transaction(isolation)
165
+ begin_db_transaction
166
+ execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
167
+ end
168
+
169
+ # Commits a transaction.
170
+ def commit_db_transaction
171
+ execute "COMMIT"
172
+ end
173
+
174
+ # Aborts a transaction.
175
+ def exec_rollback_db_transaction
176
+ execute "ROLLBACK"
177
+ end
178
+
179
+ private
180
+ def execute_batch(statements, name = nil)
181
+ execute(combine_multi_statements(statements))
182
+ end
183
+
184
+ def build_truncate_statements(table_names)
185
+ ["TRUNCATE TABLE #{table_names.map(&method(:quote_table_name)).join(", ")}"]
186
+ end
187
+
188
+ # Returns the current ID of a table's sequence.
189
+ def last_insert_id_result(sequence_name)
190
+ exec_query("SELECT currval(#{quote(sequence_name)})", "SQL")
191
+ end
192
+
193
+ def suppress_composite_primary_key(pk)
194
+ pk unless pk.is_a?(Array)
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Materialize
6
+ class ExplainPrettyPrinter # :nodoc:
7
+ # Pretty prints the result of an EXPLAIN in a way that resembles the output of the
8
+ # Materialize shell:
9
+ #
10
+ # QUERY PLAN
11
+ # ------------------------------------------------------------------------------
12
+ # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0)
13
+ # Join Filter: (posts.user_id = users.id)
14
+ # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4)
15
+ # Index Cond: (id = 1)
16
+ # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4)
17
+ # Filter: (posts.user_id = 1)
18
+ # (6 rows)
19
+ #
20
+ def pp(result)
21
+ header = result.columns.first
22
+ lines = result.rows.map(&:first)
23
+
24
+ # We add 2 because there's one char of padding at both sides, note
25
+ # the extra hyphens in the example above.
26
+ width = [header, *lines].map(&:length).max + 2
27
+
28
+ pp = []
29
+
30
+ pp << header.center(width).rstrip
31
+ pp << "-" * width
32
+
33
+ pp += lines.map { |line| " #{line}" }
34
+
35
+ nrows = result.rows.length
36
+ rows_label = nrows == 1 ? "row" : "rows"
37
+ pp << "(#{nrows} #{rows_label})"
38
+
39
+ pp.join("\n") + "\n"
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Materialize
6
+ module OID # :nodoc:
7
+ class Array < Type::Value # :nodoc:
8
+ include ActiveModel::Type::Helpers::Mutable
9
+
10
+ Data = Struct.new(:encoder, :values) # :nodoc:
11
+
12
+ attr_reader :subtype, :delimiter
13
+ delegate :type, :user_input_in_time_zone, :limit, :precision, :scale, to: :subtype
14
+
15
+ def initialize(subtype, delimiter = ",")
16
+ @subtype = subtype
17
+ @delimiter = delimiter
18
+
19
+ @pg_encoder = PG::TextEncoder::Array.new name: "#{type}[]", delimiter: delimiter
20
+ @pg_decoder = PG::TextDecoder::Array.new name: "#{type}[]", delimiter: delimiter
21
+ end
22
+
23
+ def deserialize(value)
24
+ case value
25
+ when ::String
26
+ type_cast_array(@pg_decoder.decode(value), :deserialize)
27
+ when Data
28
+ type_cast_array(value.values, :deserialize)
29
+ else
30
+ super
31
+ end
32
+ end
33
+
34
+ def cast(value)
35
+ if value.is_a?(::String)
36
+ value = begin
37
+ @pg_decoder.decode(value)
38
+ rescue TypeError
39
+ # malformed array string is treated as [], will raise in PG 2.0 gem
40
+ # this keeps a consistent implementation
41
+ []
42
+ end
43
+ end
44
+ type_cast_array(value, :cast)
45
+ end
46
+
47
+ def serialize(value)
48
+ if value.is_a?(::Array)
49
+ casted_values = type_cast_array(value, :serialize)
50
+ Data.new(@pg_encoder, casted_values)
51
+ else
52
+ super
53
+ end
54
+ end
55
+
56
+ def ==(other)
57
+ other.is_a?(Array) &&
58
+ subtype == other.subtype &&
59
+ delimiter == other.delimiter
60
+ end
61
+
62
+ def type_cast_for_schema(value)
63
+ return super unless value.is_a?(::Array)
64
+ "[" + value.map { |v| subtype.type_cast_for_schema(v) }.join(", ") + "]"
65
+ end
66
+
67
+ def map(value, &block)
68
+ value.map(&block)
69
+ end
70
+
71
+ def changed_in_place?(raw_old_value, new_value)
72
+ deserialize(raw_old_value) != new_value
73
+ end
74
+
75
+ def force_equality?(value)
76
+ value.is_a?(::Array)
77
+ end
78
+
79
+ private
80
+ def type_cast_array(value, method)
81
+ if value.is_a?(::Array)
82
+ value.map { |item| type_cast_array(item, method) }
83
+ else
84
+ @subtype.public_send(method, value)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Materialize
6
+ module OID # :nodoc:
7
+ class Bit < Type::Value # :nodoc:
8
+ def type
9
+ :bit
10
+ end
11
+
12
+ def cast_value(value)
13
+ if ::String === value
14
+ case value
15
+ when /^0x/i
16
+ value[2..-1].hex.to_s(2) # Hexadecimal notation
17
+ else
18
+ value # Bit-string notation
19
+ end
20
+ else
21
+ value.to_s
22
+ end
23
+ end
24
+
25
+ def serialize(value)
26
+ Data.new(super) if value
27
+ end
28
+
29
+ class Data
30
+ def initialize(value)
31
+ @value = value
32
+ end
33
+
34
+ def to_s
35
+ value
36
+ end
37
+
38
+ def binary?
39
+ /\A[01]*\Z/.match?(value)
40
+ end
41
+
42
+ def hex?
43
+ /\A[0-9A-F]*\Z/i.match?(value)
44
+ end
45
+
46
+ private
47
+ attr_reader :value
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Materialize
6
+ module OID # :nodoc:
7
+ class BitVarying < OID::Bit # :nodoc:
8
+ def type
9
+ :bit_varying
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Materialize
6
+ module OID # :nodoc:
7
+ class Bytea < Type::Binary # :nodoc:
8
+ def deserialize(value)
9
+ return if value.nil?
10
+ return value.to_s if value.is_a?(Type::Binary::Data)
11
+ PG::Connection.unescape_bytea(super)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ module Materialize
8
+ module OID # :nodoc:
9
+ class Cidr < Type::Value # :nodoc:
10
+ def type
11
+ :cidr
12
+ end
13
+
14
+ def type_cast_for_schema(value)
15
+ subnet_mask = value.instance_variable_get(:@mask_addr)
16
+
17
+ # If the subnet mask is equal to /32, don't output it
18
+ if subnet_mask == (2**32 - 1)
19
+ "\"#{value}\""
20
+ else
21
+ "\"#{value}/#{subnet_mask.to_s(2).count('1')}\""
22
+ end
23
+ end
24
+
25
+ def serialize(value)
26
+ if IPAddr === value
27
+ "#{value}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}"
28
+ else
29
+ value
30
+ end
31
+ end
32
+
33
+ def cast_value(value)
34
+ if value.nil?
35
+ nil
36
+ elsif String === value
37
+ begin
38
+ IPAddr.new(value)
39
+ rescue ArgumentError
40
+ nil
41
+ end
42
+ else
43
+ value
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Materialize
6
+ module OID # :nodoc:
7
+ class Date < Type::Date # :nodoc:
8
+ def cast_value(value)
9
+ case value
10
+ when "infinity" then ::Float::INFINITY
11
+ when "-infinity" then -::Float::INFINITY
12
+ when / BC$/
13
+ astronomical_year = format("%04d", -value[/^\d+/].to_i + 1)
14
+ super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year))
15
+ else
16
+ super
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Materialize
6
+ module OID # :nodoc:
7
+ class DateTime < Type::DateTime # :nodoc:
8
+ def cast_value(value)
9
+ case value
10
+ when "infinity" then ::Float::INFINITY
11
+ when "-infinity" then -::Float::INFINITY
12
+ when / BC$/
13
+ astronomical_year = format("%04d", -value[/^\d+/].to_i + 1)
14
+ super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year))
15
+ else
16
+ super
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Materialize
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,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Materialize
6
+ module OID # :nodoc:
7
+ class Enum < Type::Value # :nodoc:
8
+ def type
9
+ :enum
10
+ end
11
+
12
+ private
13
+ def cast_value(value)
14
+ value.to_s
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Materialize
6
+ module OID # :nodoc:
7
+ class Hstore < Type::Value # :nodoc:
8
+ include ActiveModel::Type::Helpers::Mutable
9
+
10
+ def type
11
+ :hstore
12
+ end
13
+
14
+ def deserialize(value)
15
+ if value.is_a?(::String)
16
+ ::Hash[value.scan(HstorePair).map { |k, v|
17
+ v = v.upcase == "NULL" ? nil : v.gsub(/\A"(.*)"\Z/m, '\1').gsub(/\\(.)/, '\1')
18
+ k = k.gsub(/\A"(.*)"\Z/m, '\1').gsub(/\\(.)/, '\1')
19
+ [k, v]
20
+ }]
21
+ else
22
+ value
23
+ end
24
+ end
25
+
26
+ def serialize(value)
27
+ if value.is_a?(::Hash)
28
+ value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(", ")
29
+ elsif value.respond_to?(:to_unsafe_h)
30
+ serialize(value.to_unsafe_h)
31
+ else
32
+ value
33
+ end
34
+ end
35
+
36
+ def accessor
37
+ ActiveRecord::Store::StringKeyedHashAccessor
38
+ end
39
+
40
+ # Will compare the Hash equivalents of +raw_old_value+ and +new_value+.
41
+ # By comparing hashes, this avoids an edge case where the order of
42
+ # the keys change between the two hashes, and they would not be marked
43
+ # as equal.
44
+ def changed_in_place?(raw_old_value, new_value)
45
+ deserialize(raw_old_value) != new_value
46
+ end
47
+
48
+ private
49
+ HstorePair = begin
50
+ quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
51
+ unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
52
+ /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
53
+ end
54
+
55
+ def escape_hstore(value)
56
+ if value.nil?
57
+ "NULL"
58
+ else
59
+ if value == ""
60
+ '""'
61
+ else
62
+ '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end