activerecord-cipherstash-pg-adapter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.tool-versions +2 -0
  4. data/CHANGELOG.md +5 -0
  5. data/Gemfile +15 -0
  6. data/LICENSE +124 -0
  7. data/README.md +29 -0
  8. data/Rakefile +8 -0
  9. data/activerecord-cipherstash-pg-adapter.gemspec +35 -0
  10. data/lib/active_record/connection_adapters/cipherstash_pg/column.rb +69 -0
  11. data/lib/active_record/connection_adapters/cipherstash_pg/database_extensions.rb +31 -0
  12. data/lib/active_record/connection_adapters/cipherstash_pg/database_statements.rb +152 -0
  13. data/lib/active_record/connection_adapters/cipherstash_pg/database_tasks.rb +15 -0
  14. data/lib/active_record/connection_adapters/cipherstash_pg/explain_pretty_printer.rb +44 -0
  15. data/lib/active_record/connection_adapters/cipherstash_pg/oid/array.rb +91 -0
  16. data/lib/active_record/connection_adapters/cipherstash_pg/oid/bit.rb +53 -0
  17. data/lib/active_record/connection_adapters/cipherstash_pg/oid/bit_varying.rb +15 -0
  18. data/lib/active_record/connection_adapters/cipherstash_pg/oid/bytea.rb +17 -0
  19. data/lib/active_record/connection_adapters/cipherstash_pg/oid/cidr.rb +48 -0
  20. data/lib/active_record/connection_adapters/cipherstash_pg/oid/date.rb +31 -0
  21. data/lib/active_record/connection_adapters/cipherstash_pg/oid/date_time.rb +36 -0
  22. data/lib/active_record/connection_adapters/cipherstash_pg/oid/decimal.rb +15 -0
  23. data/lib/active_record/connection_adapters/cipherstash_pg/oid/enum.rb +20 -0
  24. data/lib/active_record/connection_adapters/cipherstash_pg/oid/hstore.rb +109 -0
  25. data/lib/active_record/connection_adapters/cipherstash_pg/oid/inet.rb +15 -0
  26. data/lib/active_record/connection_adapters/cipherstash_pg/oid/interval.rb +49 -0
  27. data/lib/active_record/connection_adapters/cipherstash_pg/oid/jsonb.rb +15 -0
  28. data/lib/active_record/connection_adapters/cipherstash_pg/oid/legacy_point.rb +44 -0
  29. data/lib/active_record/connection_adapters/cipherstash_pg/oid/macaddr.rb +25 -0
  30. data/lib/active_record/connection_adapters/cipherstash_pg/oid/money.rb +41 -0
  31. data/lib/active_record/connection_adapters/cipherstash_pg/oid/oid.rb +15 -0
  32. data/lib/active_record/connection_adapters/cipherstash_pg/oid/point.rb +64 -0
  33. data/lib/active_record/connection_adapters/cipherstash_pg/oid/range.rb +115 -0
  34. data/lib/active_record/connection_adapters/cipherstash_pg/oid/specialized_string.rb +18 -0
  35. data/lib/active_record/connection_adapters/cipherstash_pg/oid/timestamp.rb +15 -0
  36. data/lib/active_record/connection_adapters/cipherstash_pg/oid/timestamp_with_time_zone.rb +30 -0
  37. data/lib/active_record/connection_adapters/cipherstash_pg/oid/type_map_initializer.rb +125 -0
  38. data/lib/active_record/connection_adapters/cipherstash_pg/oid/uuid.rb +35 -0
  39. data/lib/active_record/connection_adapters/cipherstash_pg/oid/vector.rb +28 -0
  40. data/lib/active_record/connection_adapters/cipherstash_pg/oid/xml.rb +30 -0
  41. data/lib/active_record/connection_adapters/cipherstash_pg/oid.rb +38 -0
  42. data/lib/active_record/connection_adapters/cipherstash_pg/quoting.rb +231 -0
  43. data/lib/active_record/connection_adapters/cipherstash_pg/referential_integrity.rb +77 -0
  44. data/lib/active_record/connection_adapters/cipherstash_pg/schema_creation.rb +100 -0
  45. data/lib/active_record/connection_adapters/cipherstash_pg/schema_definitions.rb +243 -0
  46. data/lib/active_record/connection_adapters/cipherstash_pg/schema_dumper.rb +74 -0
  47. data/lib/active_record/connection_adapters/cipherstash_pg/schema_statements.rb +812 -0
  48. data/lib/active_record/connection_adapters/cipherstash_pg/type_metadata.rb +44 -0
  49. data/lib/active_record/connection_adapters/cipherstash_pg/utils.rb +80 -0
  50. data/lib/active_record/connection_adapters/cipherstash_pg_adapter.rb +1100 -0
  51. data/lib/active_record/connection_adapters/postgres_cipherstash_adapter.rb +13 -0
  52. data/lib/activerecord-cipherstash-pg-adapter.rb +33 -0
  53. data/lib/version.rb +3 -0
  54. metadata +126 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 69d44a3c595b29d45352dc10d9db85aa5b9ebf81fec8c661ab8232af34c1571f
4
+ data.tar.gz: ddfafb6aadc8acd590ec8eabcf0e1073de6a632fbb8a74df72052cbd4da23b4d
5
+ SHA512:
6
+ metadata.gz: f349ce508471576f31638a39b37e3b46762c910cecd41f813422e7738386f5f86b6b3096a65799ac7541221866395ef885c7595d1422960546d25bb6e0145116
7
+ data.tar.gz: 70a747c73fef86e50ba4ca96e4ba088c1ca5c9b13b26819b7d56cb7dbecf3a9318388faf953a8e9a14f1b383e95e79dd24feac0dbd2589ce60d11cf541d3673f
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.tool-versions ADDED
@@ -0,0 +1,2 @@
1
+ postgres 12.4
2
+ ruby 3.1.2
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2023-02-01
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source "https://rubygems.org"
2
+ gemspec # Runtime dependencies are specified in activerecord-cipherstash-pg-adapter.gemspec
3
+
4
+ group :development, :test do
5
+ gem "rake", "~> 13.0"
6
+
7
+ gem "byebug"
8
+
9
+ # Gems used by the ActiveRecord test suite
10
+ gem "bcrypt", "~> 3.1.18"
11
+ gem "mocha", "~> 1.14.0"
12
+ gem "sqlite3", "~> 1.4.4"
13
+
14
+ gem "rspec", "~> 3.0"
15
+ end
data/LICENSE ADDED
@@ -0,0 +1,124 @@
1
+ CipherStash Client Library Licence Agreement
2
+
3
+ 0. Background
4
+
5
+ This licence sets out the terms on which you are permitted to use client side
6
+ components of software provided by CipherStash to query encrypted databases
7
+ (CipherStash Client Software).The operation of the CipherStash Client Software
8
+ is dependent on encryption keys generated by server software operated or
9
+ licensed by CipherStash.
10
+
11
+ 1. Definitions
12
+
13
+ 1.1 In these terms the following terms have the following meanings:
14
+ (a) Authorised Purpose in relation to the CipherStash Source Code has the
15
+ meaning given to it in clause 2.4;
16
+ (b) CipherStash Source Code means human readable code of the CipherStash
17
+ Client Software;
18
+ (c) CipherStash Executable means the machine executable code of the
19
+ CipherStash Client Software as made available by CipherStash from
20
+ time to time;
21
+ (d) CipherStash Client Software has the meaning given to it in the
22
+ Background;
23
+ (e) Licensed Query means a query on a database that:
24
+ (i) uses an encryption key generated by a key server operated or
25
+ licensed by CipherStash for all encryption of the content of that
26
+ query or of results returned in response to that query (excluding
27
+ encryption in the transport layer for communications between
28
+ servers); and
29
+ (ii) uses a valid token provided by CipherStash in the course of
30
+ acquiring the key referred to in the previous paragraph;
31
+ (f) Your Applications means applications that you create that rely on any
32
+ part of the CipherStash Client Software in the course of their
33
+ operation.
34
+ 1.2 In these terms, unless the context requires otherwise, references to:
35
+ (a) encryption includes decryption;
36
+ (b) keys are references to data used for encryption, not data indicating a
37
+ row in a database table.
38
+
39
+ 2. Grant of Licence
40
+
41
+ 2.1 This licence permits you to do the following in relation to the CipherStash
42
+ Client Software:
43
+ (a) use the CipherStash Executables in the course of developing and testing
44
+ Your Applications;
45
+ (b) deploy and use copies of the CipherStash Executables for the purpose of
46
+ executing Licensed Queries, including as part of one or more of Your
47
+ Applications; and
48
+ (c) use the CipherStash Source Code solely for an Authorised Purpose.
49
+ 2.2 Subject to clause 2.4(c), you must not make any modifications to the
50
+ CipherStash Client Software.
51
+ 2.3 This licence specifically excludes any use of any part of the CipherStash
52
+ Client Software to execute any queries other than Licensed Queries on any
53
+ database.
54
+ 2.4 CipherStash makes the CipherStash Source Code available for the sole purpose
55
+ of allowing third parties to verify the operation, integrity and security
56
+ of the CipherStash Client Software (Authorised Purpose). This licence
57
+ permits you to do the following solely for an Authorised Purpose:
58
+ (a) download and review the CipherStash Source Code;
59
+ (b) build executable versions of the CipherStash Source Code to verify
60
+ correspondence between it and its associated CipherStash Executable;
61
+ (c) make configuration changes to the CipherStash Source Code solely to the
62
+ extent necessary to build a working executable version under paragraph
63
+ (b).
64
+
65
+ 3. Warranties and Liability
66
+
67
+ 3.1 To the extent permitted by law, CipherStash excludes all warranties,
68
+ guarantees and conditions that would otherwise be implied into this
69
+ agreement by law. Where CipherStash is not able to exclude such a warranty,
70
+ guarantee or condition, CipherStash limits, to the extent permitted by law,
71
+ its liability for a breach of that warranty, guarantee or condition to one
72
+ or more of the following at its option:
73
+ (a) in the case of goods, any one or more of the following:
74
+ (i) the replacement of the goods or the supply of equivalent goods;
75
+ (ii) the repair of the goods;
76
+ (iii) the payment of the cost of replacing the goods or of acquiring
77
+ equivalent goods;
78
+ (iv) the payment of the cost of having the goods repaired; and
79
+ (b) in the case of services:
80
+ (i) the supplying of the services again; or
81
+ (ii) the payment of the cost of having the services supplied again.
82
+ 3.2 CipherStash has no liability to any person arising under or in relation to
83
+ this agreement (whether in tort, contract, equity or otherwise) for any
84
+ loss in the nature of consequential or economic loss. In particular,
85
+ CipherStash has no liability to any person for any: lost profits; loss of
86
+ savings, income or revenue; revenue not meeting targets or certain levels;
87
+ uptime or availability of internet connectivity or of the ability of third
88
+ parties to access a website, loss of opportunity; or loss of or corruption
89
+ of data. The exclusions in this clause 3.2 apply even in respect of loss or
90
+ damage that was foreseeable or about which either or both of the parties
91
+ were aware was likely to arise.
92
+
93
+ 4. Dispute Resolution
94
+
95
+ 4.1 Prior to commencing any action in any court or any action in any other form
96
+ of judicial or quasi-judicial forum you must comply with the requirements
97
+ of this clause 4.
98
+ 4.2 Where you believe there is a dispute between you and CipherStash in respect
99
+ of a matter the subject of this agreement you must notify CipherStash in
100
+ writing of the nature of that dispute and for a period of 120 days
101
+ following CipherStash’s receipt of that notification, make reasonable
102
+ attempts to resolve that dispute with CipherStash.
103
+
104
+ 5. General and Interpretation
105
+
106
+ 5.1 Except where expressly set out to the contrary, nothing in this agreement
107
+ grants the Customer any rights over any intellectual property rights
108
+ (including copyright, patents, and rights to the registration of such
109
+ rights) held by CipherStash at any time.
110
+ 5.2 No provision of this agreement may be construed against a party because
111
+ that party drafted that term.
112
+ 5.3 A waiver of rights under this agreement can only occur in writing signed by
113
+ the party granting the waiver. Except to the extent set out in the waiver,
114
+ a waiver is only effective in relation to the specific facts and rights set
115
+ out in it and does not operate to waive any other rights or to waive the
116
+ same rights in respect of different facts or circumstances.
117
+ 5.4 Where a part of this agreement is held by a court to be illegal or
118
+ otherwise unenforceable, and the unenforceability of that part does not
119
+ substantially alter the character of the bargain that would have been in
120
+ existence between the parties had that part been enforceable, that part is
121
+ severed and the balance of this agreement will continue unaffected.
122
+ 5.5 This contract is governed by the laws in force in the State of New South
123
+ Wales, Australia. Each party submits to the non-exclusive jurisdiction of
124
+ the courts of that State.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # ActiveRecord CipherStash PostgreSQL Adapter
2
+
3
+ An adapter to allow the use of the CipherStash libpq fork for encryption of data in your PostgreSQL databases.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'activerecord-cipherstash-pg-adapter'
11
+ ```
12
+
13
+ In `database.yml`, use the following adapter setting:
14
+
15
+ ```yaml
16
+ development:
17
+ adapter: cipherstash_pg
18
+ # ... username, password, etc. as you would with postgres as normal.
19
+ ```
20
+
21
+ ## Development
22
+
23
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
24
+
25
+ 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).
26
+
27
+ ## Contributing
28
+
29
+ Bug reports and pull requests are welcome on GitHub at https://github.com/cipherstash/activerecord-cipherstash-pg-adapter
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,35 @@
1
+ require_relative "lib/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "activerecord-cipherstash-pg-adapter"
5
+ spec.version = ActiveRecord::CIPHERSTASH_PG_ADAPTER_VERSION
6
+ spec.authors = ["Robin Howard"]
7
+ spec.email = ["robin@cipherstash.com"]
8
+
9
+ spec.summary = "CipherStash PostgreSQL adapter for ActiveRecord."
10
+ spec.description = spec.summary
11
+ spec.homepage = "https://github.com/cipherstash/activerecord-cipherstash-pg-adapter"
12
+ spec.license = "LicenseRef-LICENCE"
13
+
14
+ spec.metadata["homepage_uri"] = spec.homepage
15
+ spec.metadata["source_code_uri"] = "#{spec.homepage}.git"
16
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
17
+
18
+ spec.required_ruby_version = ">= 2.6.0"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
25
+ end
26
+ end
27
+
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
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"
35
+ end
@@ -0,0 +1,69 @@
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, generated: nil, **)
12
+ super
13
+ @serial = serial
14
+ @generated = generated
15
+ end
16
+
17
+ def serial?
18
+ @serial
19
+ end
20
+
21
+ def virtual?
22
+ # We assume every generated column is virtual, no matter the concrete type
23
+ @generated.present?
24
+ end
25
+
26
+ def has_default?
27
+ super && !virtual?
28
+ end
29
+
30
+ def array
31
+ sql_type_metadata.sql_type.end_with?("[]")
32
+ end
33
+ alias :array? :array
34
+
35
+ def enum?
36
+ type == :enum
37
+ end
38
+
39
+ def sql_type
40
+ super.delete_suffix("[]")
41
+ end
42
+
43
+ def init_with(coder)
44
+ @serial = coder["serial"]
45
+ super
46
+ end
47
+
48
+ def encode_with(coder)
49
+ coder["serial"] = @serial
50
+ super
51
+ end
52
+
53
+ def ==(other)
54
+ other.is_a?(Column) &&
55
+ super &&
56
+ serial? == other.serial?
57
+ end
58
+ alias :eql? :==
59
+
60
+ def hash
61
+ Column.hash ^
62
+ super.hash ^
63
+ serial?.hash
64
+ end
65
+ end
66
+ end
67
+ CipherStashPGColumn = CipherStashPG::Column # :nodoc:
68
+ end
69
+ end
@@ -0,0 +1,31 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module CipherStashPG
4
+ module DatabaseExtensions
5
+ def self.install
6
+ logger.info("Installing database extension.....")
7
+
8
+ ActiveRecord::Base.connection.execute(
9
+ ::CipherStash::PG.install_script
10
+ )
11
+
12
+ logger.info("Database extension installed.")
13
+ end
14
+
15
+ def self.uninstall
16
+ logger.info("Uninstalling database extension.....")
17
+
18
+ ActiveRecord::Base.connection.execute(
19
+ ::CipherStash::PG.uninstall_script
20
+ )
21
+
22
+ logger.info("Database extension uninstalled.")
23
+ end
24
+
25
+ private_class_method def self.logger
26
+ @logger ||= Rails.logger
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,152 @@
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
+ sql = transform_query(sql)
41
+ check_if_write_query(sql)
42
+
43
+ materialize_transactions
44
+ mark_transaction_written_if_write(sql)
45
+
46
+ log(sql, name) do
47
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
48
+ @connection.async_exec(sql)
49
+ end
50
+ end
51
+ end
52
+
53
+ def exec_query(sql, name = "SQL", binds = [], prepare: false, async: false) # :nodoc:
54
+ execute_and_clear(sql, name, binds, prepare: prepare, async: async) do |result|
55
+ types = {}
56
+ fields = result.fields
57
+ fields.each_with_index do |fname, i|
58
+ ftype = result.ftype i
59
+ fmod = result.fmod i
60
+ types[fname] = get_oid_type(ftype, fmod, fname)
61
+ end
62
+ build_result(columns: fields, rows: result.values, column_types: types)
63
+ end
64
+ end
65
+
66
+ def exec_delete(sql, name = nil, binds = []) # :nodoc:
67
+ execute_and_clear(sql, name, binds) { |result| result.cmd_tuples }
68
+ end
69
+ alias :exec_update :exec_delete
70
+
71
+ def sql_for_insert(sql, pk, binds) # :nodoc:
72
+ if pk.nil?
73
+ # Extract the table from the insert sql. Yuck.
74
+ table_ref = extract_table_ref_from_insert_sql(sql)
75
+ pk = primary_key(table_ref) if table_ref
76
+ end
77
+
78
+ if pk = suppress_composite_primary_key(pk)
79
+ sql = "#{sql} RETURNING #{quote_column_name(pk)}"
80
+ end
81
+
82
+ super
83
+ end
84
+ private :sql_for_insert
85
+
86
+ def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil) # :nodoc:
87
+ if use_insert_returning? || pk == false
88
+ super
89
+ else
90
+ result = exec_query(sql, name, binds)
91
+ unless sequence_name
92
+ table_ref = extract_table_ref_from_insert_sql(sql)
93
+ if table_ref
94
+ pk = primary_key(table_ref) if pk.nil?
95
+ pk = suppress_composite_primary_key(pk)
96
+ sequence_name = default_sequence_name(table_ref, pk)
97
+ end
98
+ return result unless sequence_name
99
+ end
100
+ last_insert_id_result(sequence_name)
101
+ end
102
+ end
103
+
104
+ # Begins a transaction.
105
+ def begin_db_transaction # :nodoc:
106
+ execute("BEGIN", "TRANSACTION")
107
+ end
108
+
109
+ def begin_isolated_db_transaction(isolation) # :nodoc:
110
+ begin_db_transaction
111
+ execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
112
+ end
113
+
114
+ # Commits a transaction.
115
+ def commit_db_transaction # :nodoc:
116
+ execute("COMMIT", "TRANSACTION")
117
+ end
118
+
119
+ # Aborts a transaction.
120
+ def exec_rollback_db_transaction # :nodoc:
121
+ execute("ROLLBACK", "TRANSACTION")
122
+ end
123
+
124
+ # From https://www.cipherstash_pg.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-CURRENT
125
+ HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("CURRENT_TIMESTAMP").freeze # :nodoc:
126
+ private_constant :HIGH_PRECISION_CURRENT_TIMESTAMP
127
+
128
+ def high_precision_current_timestamp
129
+ HIGH_PRECISION_CURRENT_TIMESTAMP
130
+ end
131
+
132
+ private
133
+ def execute_batch(statements, name = nil)
134
+ execute(combine_multi_statements(statements))
135
+ end
136
+
137
+ def build_truncate_statements(table_names)
138
+ ["TRUNCATE TABLE #{table_names.map(&method(:quote_table_name)).join(", ")}"]
139
+ end
140
+
141
+ # Returns the current ID of a table's sequence.
142
+ def last_insert_id_result(sequence_name)
143
+ exec_query("SELECT currval(#{quote(sequence_name)})", "SQL")
144
+ end
145
+
146
+ def suppress_composite_primary_key(pk)
147
+ pk unless pk.is_a?(Array)
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,15 @@
1
+ require "active_record/base"
2
+
3
+ require "active_record/tasks/postgresql_database_tasks"
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ module CipherStashPG
8
+ class DatabaseTasks < ActiveRecord::Tasks::PostgreSQLDatabaseTasks
9
+ # nop; just here for the inheritance.
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ ActiveRecord::Tasks::DatabaseTasks.register_task(/cipherstash_pg/, ActiveRecord::ConnectionAdapters::CipherStashPG::DatabaseTasks)
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module CipherStashPG
6
+ class ExplainPrettyPrinter # :nodoc:
7
+ # Pretty prints the result of an EXPLAIN in a way that resembles the output of the
8
+ # PostgreSQL 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 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 { |v| subtype.map(v, &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