activerecord-cipherstash-pg-adapter 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/README.md +11 -1
  4. data/activerecord-cipherstash-pg-adapter.gemspec +2 -2
  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 +93 -50
  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: 434fd7249bd70c44846fa14a0d1ece70776c0ef79ee4632ca39f15b6036e49dd
4
+ data.tar.gz: b54aa489eb0053e8a065dc7d938cc35f7ced39b3c0cd06d41b13a55b7494c080
5
5
  SHA512:
6
- metadata.gz: b25b5b4ef6ad537cb9ec045458553566b743b9739b3c0c863d41d28dc78316e9f557904c55d6145789bcf85b7bb15c9d42f5a81cb8b411471f4516274763e37d
7
- data.tar.gz: be6d02946c7e8df400dc68d13bc614fd28a3787dea5b9317e8d4e86773097f2a22f7ced6dd5ca3b6fe58f8bfb8fdd041f64fbc0453bae3e9ad2bc89d87a2ffa2
6
+ metadata.gz: 7ad1fb8b78eb0ca2bf4796c1096dfbc58ab2efa4b435d9f5f70db0dbe0a7fbcf5399944ea969a75da2754d04116930061fbadb2e95afa61b014c622b26214e25
7
+ data.tar.gz: b8b577f348e757cb2c0da27096d3475f14dbe9d9a555d80d8e1e8e25fdd7238713509167abbeaaff4129d5b939dfc7df4d3f49cd5ab992a92975eea54e387646
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2023-04-06
4
+
5
+ ### Changed
6
+
7
+ - Bump version of cipherstash-pg.
8
+
9
+ ## [0.3.0] - 2023-04-04
10
+ ### Added
11
+
12
+ - Support for both Rails 6 + 7.
13
+
14
+ ## [0.2.0] - 2023-03-29
15
+
16
+ ### Added
17
+
18
+ - Rake task to migrate plaintext data to encrypted columns.
19
+
20
+ ### Fixed
21
+
22
+ - Rails commands not recognised.
23
+
3
24
  ## [0.1.0] - 2023-02-01
4
25
 
5
26
  - 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"
34
- spec.add_dependency "cipherstash-pg", "~> 1.4.5"
33
+ spec.add_dependency "activerecord", ">= 6.0.0", "< 8.0.0"
34
+ spec.add_dependency "cipherstash-pg", ">= 1.0.0.beta.1"
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