activerecord-cipherstash-pg-adapter 0.2.0 → 0.3.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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/README.md +11 -1
  4. data/activerecord-cipherstash-pg-adapter.gemspec +1 -1
  5. data/lib/active_record/connection_adapters/6.1/cipherstash_pg/column.rb +55 -0
  6. data/lib/active_record/connection_adapters/6.1/cipherstash_pg/database_statements.rb +149 -0
  7. data/lib/active_record/connection_adapters/6.1/cipherstash_pg/oid/array.rb +91 -0
  8. data/lib/active_record/connection_adapters/6.1/cipherstash_pg/oid/date.rb +23 -0
  9. data/lib/active_record/connection_adapters/6.1/cipherstash_pg/oid/date_time.rb +31 -0
  10. data/lib/active_record/connection_adapters/6.1/cipherstash_pg/oid/hstore.rb +72 -0
  11. data/lib/active_record/connection_adapters/6.1/cipherstash_pg/oid/type_map_initializer.rb +113 -0
  12. data/lib/active_record/connection_adapters/6.1/cipherstash_pg/oid.rb +36 -0
  13. data/lib/active_record/connection_adapters/6.1/cipherstash_pg/quoting.rb +205 -0
  14. data/lib/active_record/connection_adapters/6.1/cipherstash_pg/referential_integrity.rb +43 -0
  15. data/lib/active_record/connection_adapters/6.1/cipherstash_pg/schema_creation.rb +80 -0
  16. data/lib/active_record/connection_adapters/6.1/cipherstash_pg/schema_definitions.rb +222 -0
  17. data/lib/active_record/connection_adapters/6.1/cipherstash_pg/schema_dumper.rb +49 -0
  18. data/lib/active_record/connection_adapters/6.1/cipherstash_pg/schema_statements.rb +794 -0
  19. data/lib/active_record/connection_adapters/6.1/postgres_cipherstash_adapter.rb +958 -0
  20. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/explain_pretty_printer.rb +44 -0
  21. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid/bit.rb +53 -0
  22. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid/bit_varying.rb +15 -0
  23. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid/bytea.rb +17 -0
  24. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid/cidr.rb +48 -0
  25. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid/decimal.rb +15 -0
  26. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid/enum.rb +20 -0
  27. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid/inet.rb +15 -0
  28. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid/interval.rb +49 -0
  29. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid/jsonb.rb +15 -0
  30. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid/legacy_point.rb +44 -0
  31. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid/macaddr.rb +25 -0
  32. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid/money.rb +41 -0
  33. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid/oid.rb +15 -0
  34. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid/point.rb +64 -0
  35. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid/range.rb +115 -0
  36. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid/specialized_string.rb +18 -0
  37. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid/uuid.rb +35 -0
  38. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid/vector.rb +28 -0
  39. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid/xml.rb +30 -0
  40. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/oid.rb +38 -0
  41. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/type_metadata.rb +44 -0
  42. data/lib/active_record/connection_adapters/7.0/cipherstash_pg/utils.rb +80 -0
  43. data/lib/active_record/connection_adapters/{cipherstash_pg_adapter.rb → 7.0/postgres_cipherstash_adapter.rb} +16 -48
  44. data/lib/active_record/connection_adapters/postgres_cipherstash_adapter.rb +42 -12
  45. data/lib/activerecord-cipherstash-pg-adapter.rb +1 -1
  46. data/lib/version.rb +1 -1
  47. metadata +89 -46
  48. data/lib/active_record/connection_adapters/cipherstash_pg/oid.rb +0 -38
  49. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/explain_pretty_printer.rb +0 -0
  50. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/oid/bit.rb +0 -0
  51. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/oid/bit_varying.rb +0 -0
  52. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/oid/bytea.rb +0 -0
  53. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/oid/cidr.rb +0 -0
  54. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/oid/decimal.rb +0 -0
  55. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/oid/enum.rb +0 -0
  56. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/oid/inet.rb +0 -0
  57. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/oid/interval.rb +0 -0
  58. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/oid/jsonb.rb +0 -0
  59. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/oid/legacy_point.rb +0 -0
  60. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/oid/macaddr.rb +0 -0
  61. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/oid/money.rb +0 -0
  62. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/oid/oid.rb +0 -0
  63. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/oid/point.rb +0 -0
  64. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/oid/range.rb +0 -0
  65. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/oid/specialized_string.rb +0 -0
  66. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/oid/uuid.rb +0 -0
  67. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/oid/vector.rb +0 -0
  68. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/oid/xml.rb +0 -0
  69. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/type_metadata.rb +0 -0
  70. /data/lib/active_record/connection_adapters/{cipherstash_pg → 6.1/cipherstash_pg}/utils.rb +0 -0
  71. /data/lib/active_record/connection_adapters/{cipherstash_pg → 7.0/cipherstash_pg}/column.rb +0 -0
  72. /data/lib/active_record/connection_adapters/{cipherstash_pg → 7.0/cipherstash_pg}/database_statements.rb +0 -0
  73. /data/lib/active_record/connection_adapters/{cipherstash_pg → 7.0/cipherstash_pg}/oid/array.rb +0 -0
  74. /data/lib/active_record/connection_adapters/{cipherstash_pg → 7.0/cipherstash_pg}/oid/date.rb +0 -0
  75. /data/lib/active_record/connection_adapters/{cipherstash_pg → 7.0/cipherstash_pg}/oid/date_time.rb +0 -0
  76. /data/lib/active_record/connection_adapters/{cipherstash_pg → 7.0/cipherstash_pg}/oid/hstore.rb +0 -0
  77. /data/lib/active_record/connection_adapters/{cipherstash_pg → 7.0/cipherstash_pg}/oid/timestamp.rb +0 -0
  78. /data/lib/active_record/connection_adapters/{cipherstash_pg → 7.0/cipherstash_pg}/oid/timestamp_with_time_zone.rb +0 -0
  79. /data/lib/active_record/connection_adapters/{cipherstash_pg → 7.0/cipherstash_pg}/oid/type_map_initializer.rb +0 -0
  80. /data/lib/active_record/connection_adapters/{cipherstash_pg → 7.0/cipherstash_pg}/quoting.rb +0 -0
  81. /data/lib/active_record/connection_adapters/{cipherstash_pg → 7.0/cipherstash_pg}/referential_integrity.rb +0 -0
  82. /data/lib/active_record/connection_adapters/{cipherstash_pg → 7.0/cipherstash_pg}/schema_creation.rb +0 -0
  83. /data/lib/active_record/connection_adapters/{cipherstash_pg → 7.0/cipherstash_pg}/schema_definitions.rb +0 -0
  84. /data/lib/active_record/connection_adapters/{cipherstash_pg → 7.0/cipherstash_pg}/schema_dumper.rb +0 -0
  85. /data/lib/active_record/connection_adapters/{cipherstash_pg → 7.0/cipherstash_pg}/schema_statements.rb +0 -0
  86. /data/lib/{active_record/connection_adapters/cipherstash_pg/cipherstash_tasks.rake → cipherstash_tasks.rake} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 17eaea76f767bfda72f26bd7c9d89a4e8e0333dfc19be95a5390cbb8b0cbfc44
4
- data.tar.gz: 379044af26990eb20e563e9aaa0bf727fcb5e70ac63ec89afa86e648d8882d3c
3
+ metadata.gz: 3eaa0c02e8ccecc531934417632599e4f91edcd67fed514f315ebef2bc424ee8
4
+ data.tar.gz: 369c9295c615b3468421bcef9142421ac767c4dd6ef3f4a46139ce0183adc37f
5
5
  SHA512:
6
- metadata.gz: b25b5b4ef6ad537cb9ec045458553566b743b9739b3c0c863d41d28dc78316e9f557904c55d6145789bcf85b7bb15c9d42f5a81cb8b411471f4516274763e37d
7
- data.tar.gz: be6d02946c7e8df400dc68d13bc614fd28a3787dea5b9317e8d4e86773097f2a22f7ced6dd5ca3b6fe58f8bfb8fdd041f64fbc0453bae3e9ad2bc89d87a2ffa2
6
+ metadata.gz: '09b55773769ab21a814208fd25058a82cad19a6c047ccbc84d4a715447bdcfade7b68e399242049e0285c26d0d22725310a02cb62fc4532621d6f095d90e6a93'
7
+ data.tar.gz: a74e2ead6f0c3029725fbd2b4d1e0b76c228446b8b284a954ce737e8ae488743105baa0f356ad922d5fdedd23051d3059b2a6618cad30b507fe6a2554cc7b3b9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2023-04-04
4
+ ### Added
5
+
6
+ - Support for both Rails 6 + 7.
7
+
8
+ ## [0.2.0] - 2023-03-29
9
+
10
+ ### Added
11
+
12
+ - Rake task to migrate plaintext data to encrypted columns.
13
+
14
+ ### Fixed
15
+
16
+ - Rails commands not recognised.
17
+
3
18
  ## [0.1.0] - 2023-02-01
4
19
 
5
20
  - Initial release
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  An adapter to allow the use of the CipherStash libpq fork for encryption of data in your PostgreSQL databases.
4
4
 
5
+ This adapter supports Rails 6 & 7.
6
+
5
7
  ## Installation
6
8
 
7
9
  Add this line to your application's Gemfile:
@@ -16,7 +18,7 @@ In `database.yml`, use the following adapter setting:
16
18
 
17
19
  ```yaml
18
20
  development:
19
- adapter: cipherstash_pg
21
+ adapter: postgres_cipherstash
20
22
  # ... username, password, etc. as you would with postgres as normal.
21
23
  ```
22
24
 
@@ -26,6 +28,14 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
26
28
 
27
29
  To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
28
30
 
31
+ In the github repo:
32
+
33
+ - Click on releases.
34
+ - Click on `Draft a new release`.
35
+ - Select the created tag from the `Choose a tag` dropdown.
36
+ - Click on `Generate release notes`
37
+ - Click on `Publish release`.
38
+
29
39
  ## Contributing
30
40
 
31
41
  Bug reports and pull requests are welcome on GitHub at https://github.com/cipherstash/activerecord-cipherstash-pg-adapter
@@ -30,6 +30,6 @@ Gem::Specification.new do |spec|
30
30
  spec.require_paths = ["lib"]
31
31
 
32
32
  # Runtime dependencies here; dev+test go in Gemfile.
33
- spec.add_dependency "activerecord", "~> 7.0.3"
33
+ spec.add_dependency "activerecord", ">= 6.0.0", "< 8.0.0"
34
34
  spec.add_dependency "cipherstash-pg", "~> 1.4.5"
35
35
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/blank"
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ module CipherStashPG
8
+ class Column < ConnectionAdapters::Column # :nodoc:
9
+ delegate :oid, :fmod, to: :sql_type_metadata
10
+
11
+ def initialize(*, serial: nil, **)
12
+ super
13
+ @serial = serial
14
+ end
15
+
16
+ def serial?
17
+ @serial
18
+ end
19
+
20
+ def array
21
+ sql_type_metadata.sql_type.end_with?("[]")
22
+ end
23
+ alias :array? :array
24
+
25
+ def sql_type
26
+ super.delete_suffix("[]")
27
+ end
28
+
29
+ def init_with(coder)
30
+ @serial = coder["serial"]
31
+ super
32
+ end
33
+
34
+ def encode_with(coder)
35
+ coder["serial"] = @serial
36
+ super
37
+ end
38
+
39
+ def ==(other)
40
+ other.is_a?(Column) &&
41
+ super &&
42
+ serial? == other.serial?
43
+ end
44
+ alias :eql? :==
45
+
46
+ def hash
47
+ Column.hash ^
48
+ super.hash ^
49
+ serial?.hash
50
+ end
51
+ end
52
+ end
53
+ CipherStashPGColumn = CipherStashPG::Column # :nodoc:
54
+ end
55
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module CipherStashPG
6
+ module DatabaseStatements
7
+ def explain(arel, binds = [])
8
+ sql = "EXPLAIN #{to_sql(arel, binds)}"
9
+ CipherStashPG::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", binds))
10
+ end
11
+
12
+ # Queries the database and returns the results in an Array-like object
13
+ def query(sql, name = nil) # :nodoc:
14
+ materialize_transactions
15
+ mark_transaction_written_if_write(sql)
16
+
17
+ log(sql, name) do
18
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
19
+ @connection.async_exec(sql).map_types!(@type_map_for_results).values
20
+ end
21
+ end
22
+ end
23
+
24
+ READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(
25
+ :close, :declare, :fetch, :move, :set, :show
26
+ ) # :nodoc:
27
+ private_constant :READ_QUERY
28
+
29
+ def write_query?(sql) # :nodoc:
30
+ !READ_QUERY.match?(sql)
31
+ rescue ArgumentError # Invalid encoding
32
+ !READ_QUERY.match?(sql.b)
33
+ end
34
+
35
+ # Executes an SQL statement, returning a PG::Result object on success
36
+ # or raising a PG::Error exception otherwise.
37
+ # Note: the PG::Result object is manually memory managed; if you don't
38
+ # need it specifically, you may want consider the <tt>exec_query</tt> wrapper.
39
+ def execute(sql, name = nil)
40
+ if preventing_writes? && write_query?(sql)
41
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
42
+ end
43
+
44
+ materialize_transactions
45
+ mark_transaction_written_if_write(sql)
46
+
47
+ log(sql, name) do
48
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
49
+ @connection.async_exec(sql)
50
+ end
51
+ end
52
+ end
53
+
54
+ def exec_query(sql, name = "SQL", binds = [], prepare: false)
55
+ execute_and_clear(sql, name, binds, prepare: prepare) do |result|
56
+ types = {}
57
+ fields = result.fields
58
+ fields.each_with_index do |fname, i|
59
+ ftype = result.ftype i
60
+ fmod = result.fmod i
61
+ case type = get_oid_type(ftype, fmod, fname)
62
+ when Type::Integer, Type::Float, OID::Decimal, Type::String, Type::DateTime, Type::Boolean
63
+ # skip if a column has already been type casted by pg decoders
64
+ else types[fname] = type
65
+ end
66
+ end
67
+ build_result(columns: fields, rows: result.values, column_types: types)
68
+ end
69
+ end
70
+
71
+ def exec_delete(sql, name = nil, binds = [])
72
+ execute_and_clear(sql, name, binds) { |result| result.cmd_tuples }
73
+ end
74
+ alias :exec_update :exec_delete
75
+
76
+ def sql_for_insert(sql, pk, binds) # :nodoc:
77
+ if pk.nil?
78
+ # Extract the table from the insert sql. Yuck.
79
+ table_ref = extract_table_ref_from_insert_sql(sql)
80
+ pk = primary_key(table_ref) if table_ref
81
+ end
82
+
83
+ if pk = suppress_composite_primary_key(pk)
84
+ sql = "#{sql} RETURNING #{quote_column_name(pk)}"
85
+ end
86
+
87
+ super
88
+ end
89
+ private :sql_for_insert
90
+
91
+ def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil) # :nodoc:
92
+ if use_insert_returning? || pk == false
93
+ super
94
+ else
95
+ result = exec_query(sql, name, binds)
96
+ unless sequence_name
97
+ table_ref = extract_table_ref_from_insert_sql(sql)
98
+ if table_ref
99
+ pk = primary_key(table_ref) if pk.nil?
100
+ pk = suppress_composite_primary_key(pk)
101
+ sequence_name = default_sequence_name(table_ref, pk)
102
+ end
103
+ return result unless sequence_name
104
+ end
105
+ last_insert_id_result(sequence_name)
106
+ end
107
+ end
108
+
109
+ # Begins a transaction.
110
+ def begin_db_transaction # :nodoc:
111
+ execute("BEGIN", "TRANSACTION")
112
+ end
113
+
114
+ def begin_isolated_db_transaction(isolation) # :nodoc:
115
+ begin_db_transaction
116
+ execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
117
+ end
118
+
119
+ # Commits a transaction.
120
+ def commit_db_transaction # :nodoc:
121
+ execute("COMMIT", "TRANSACTION")
122
+ end
123
+
124
+ # Aborts a transaction.
125
+ def exec_rollback_db_transaction # :nodoc:
126
+ execute("ROLLBACK", "TRANSACTION")
127
+ end
128
+
129
+ private
130
+ def execute_batch(statements, name = nil)
131
+ execute(combine_multi_statements(statements))
132
+ end
133
+
134
+ def build_truncate_statements(table_names)
135
+ ["TRUNCATE TABLE #{table_names.map(&method(:quote_table_name)).join(", ")}"]
136
+ end
137
+
138
+ # Returns the current ID of a table's sequence.
139
+ def last_insert_id_result(sequence_name)
140
+ exec_query("SELECT currval(#{quote(sequence_name)})", "SQL")
141
+ end
142
+
143
+ def suppress_composite_primary_key(pk)
144
+ pk unless pk.is_a?(Array)
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module CipherStashPG
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,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module CipherStashPG
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
+ value = value.sub(/^\d+/) { |year| format("%04d", -year.to_i + 1) }
14
+ super(value.delete_suffix!(" BC"))
15
+ else
16
+ super
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module CipherStashPG
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
+ value = value.sub(/^\d+/) { |year| format("%04d", -year.to_i + 1) }
14
+ super(value.delete_suffix!(" BC"))
15
+ else
16
+ super
17
+ end
18
+ end
19
+
20
+ def type_cast_for_schema(value)
21
+ case value
22
+ when ::Float::INFINITY then "::Float::INFINITY"
23
+ when -::Float::INFINITY then "-::Float::INFINITY"
24
+ else super
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ module CipherStashPG
8
+ module OID # :nodoc:
9
+ class Hstore < Type::Value # :nodoc:
10
+ include ActiveModel::Type::Helpers::Mutable
11
+
12
+ def type
13
+ :hstore
14
+ end
15
+
16
+ def deserialize(value)
17
+ if value.is_a?(::String)
18
+ ::Hash[value.scan(HstorePair).map { |k, v|
19
+ v = v.upcase == "NULL" ? nil : v.gsub(/\A"(.*)"\Z/m, '\1').gsub(/\\(.)/, '\1')
20
+ k = k.gsub(/\A"(.*)"\Z/m, '\1').gsub(/\\(.)/, '\1')
21
+ [k, v]
22
+ }]
23
+ else
24
+ value
25
+ end
26
+ end
27
+
28
+ def serialize(value)
29
+ if value.is_a?(::Hash)
30
+ value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(", ")
31
+ elsif value.respond_to?(:to_unsafe_h)
32
+ serialize(value.to_unsafe_h)
33
+ else
34
+ value
35
+ end
36
+ end
37
+
38
+ def accessor
39
+ ActiveRecord::Store::StringKeyedHashAccessor
40
+ end
41
+
42
+ # Will compare the Hash equivalents of +raw_old_value+ and +new_value+.
43
+ # By comparing hashes, this avoids an edge case where the order of
44
+ # the keys change between the two hashes, and they would not be marked
45
+ # as equal.
46
+ def changed_in_place?(raw_old_value, new_value)
47
+ deserialize(raw_old_value) != new_value
48
+ end
49
+
50
+ private
51
+ HstorePair = begin
52
+ quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
53
+ unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
54
+ /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
55
+ end
56
+
57
+ def escape_hstore(value)
58
+ if value.nil?
59
+ "NULL"
60
+ else
61
+ if value == ""
62
+ '""'
63
+ else
64
+ '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/array/extract"
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ module CipherStashPG
8
+ module OID # :nodoc:
9
+ # This class uses the data from PostgreSQL pg_type table to build
10
+ # the OID -> Type mapping.
11
+ # - OID is an integer representing the type.
12
+ # - Type is an OID::Type object.
13
+ # This class has side effects on the +store+ passed during initialization.
14
+ class TypeMapInitializer # :nodoc:
15
+ def initialize(store)
16
+ @store = store
17
+ end
18
+
19
+ def run(records)
20
+ nodes = records.reject { |row| @store.key? row["oid"].to_i }
21
+ mapped = nodes.extract! { |row| @store.key? row["typname"] }
22
+ ranges = nodes.extract! { |row| row["typtype"] == "r" }
23
+ enums = nodes.extract! { |row| row["typtype"] == "e" }
24
+ domains = nodes.extract! { |row| row["typtype"] == "d" }
25
+ arrays = nodes.extract! { |row| row["typinput"] == "array_in" }
26
+ composites = nodes.extract! { |row| row["typelem"].to_i != 0 }
27
+
28
+ mapped.each { |row| register_mapped_type(row) }
29
+ enums.each { |row| register_enum_type(row) }
30
+ domains.each { |row| register_domain_type(row) }
31
+ arrays.each { |row| register_array_type(row) }
32
+ ranges.each { |row| register_range_type(row) }
33
+ composites.each { |row| register_composite_type(row) }
34
+ end
35
+
36
+ def query_conditions_for_initial_load
37
+ known_type_names = @store.keys.map { |n| "'#{n}'" }
38
+ known_type_types = %w('r' 'e' 'd')
39
+ <<~SQL % [known_type_names.join(", "), known_type_types.join(", ")]
40
+ WHERE
41
+ t.typname IN (%s)
42
+ OR t.typtype IN (%s)
43
+ OR t.typinput = 'array_in(cstring,oid,integer)'::regprocedure
44
+ OR t.typelem != 0
45
+ SQL
46
+ end
47
+
48
+ private
49
+ def register_mapped_type(row)
50
+ alias_type row["oid"], row["typname"]
51
+ end
52
+
53
+ def register_enum_type(row)
54
+ register row["oid"], OID::Enum.new
55
+ end
56
+
57
+ def register_array_type(row)
58
+ register_with_subtype(row["oid"], row["typelem"].to_i) do |subtype|
59
+ OID::Array.new(subtype, row["typdelim"])
60
+ end
61
+ end
62
+
63
+ def register_range_type(row)
64
+ register_with_subtype(row["oid"], row["rngsubtype"].to_i) do |subtype|
65
+ OID::Range.new(subtype, row["typname"].to_sym)
66
+ end
67
+ end
68
+
69
+ def register_domain_type(row)
70
+ if base_type = @store.lookup(row["typbasetype"].to_i)
71
+ register row["oid"], base_type
72
+ else
73
+ warn "unknown base type (OID: #{row["typbasetype"]}) for domain #{row["typname"]}."
74
+ end
75
+ end
76
+
77
+ def register_composite_type(row)
78
+ if subtype = @store.lookup(row["typelem"].to_i)
79
+ register row["oid"], OID::Vector.new(row["typdelim"], subtype)
80
+ end
81
+ end
82
+
83
+ def register(oid, oid_type = nil, &block)
84
+ oid = assert_valid_registration(oid, oid_type || block)
85
+ if block_given?
86
+ @store.register_type(oid, &block)
87
+ else
88
+ @store.register_type(oid, oid_type)
89
+ end
90
+ end
91
+
92
+ def alias_type(oid, target)
93
+ oid = assert_valid_registration(oid, target)
94
+ @store.alias_type(oid, target)
95
+ end
96
+
97
+ def register_with_subtype(oid, target_oid)
98
+ if @store.key?(target_oid)
99
+ register(oid) do |_, *args|
100
+ yield @store.lookup(target_oid, *args)
101
+ end
102
+ end
103
+ end
104
+
105
+ def assert_valid_registration(oid, oid_type)
106
+ raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil?
107
+ oid.to_i
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./oid/array"
4
+ require_relative "./oid/bit"
5
+ require_relative "./oid/bit_varying"
6
+ require_relative "./oid/bytea"
7
+ require_relative "./oid/cidr"
8
+ require_relative "./oid/date"
9
+ require_relative "./oid/date_time"
10
+ require_relative "./oid/decimal"
11
+ require_relative "./oid/enum"
12
+ require_relative "./oid/hstore"
13
+ require_relative "./oid/inet"
14
+ require_relative "./oid/interval"
15
+ require_relative "./oid/jsonb"
16
+ require_relative "./oid/macaddr"
17
+ require_relative "./oid/money"
18
+ require_relative "./oid/oid"
19
+ require_relative "./oid/point"
20
+ require_relative "./oid/legacy_point"
21
+ require_relative "./oid/range"
22
+ require_relative "./oid/specialized_string"
23
+ require_relative "./oid/uuid"
24
+ require_relative "./oid/vector"
25
+ require_relative "./oid/xml"
26
+
27
+ require_relative "./oid/type_map_initializer"
28
+
29
+ module ActiveRecord
30
+ module ConnectionAdapters
31
+ module CipherStashPG
32
+ module OID # :nodoc:
33
+ end
34
+ end
35
+ end
36
+ end