activerecord-cipherstash-pg-adapter 0.1.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 (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