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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.tool-versions +2 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +15 -0
- data/LICENSE +124 -0
- data/README.md +29 -0
- data/Rakefile +8 -0
- data/activerecord-cipherstash-pg-adapter.gemspec +35 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/column.rb +69 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/database_extensions.rb +31 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/database_statements.rb +152 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/database_tasks.rb +15 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/explain_pretty_printer.rb +44 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/array.rb +91 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/bit.rb +53 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/bit_varying.rb +15 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/bytea.rb +17 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/cidr.rb +48 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/date.rb +31 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/date_time.rb +36 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/decimal.rb +15 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/enum.rb +20 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/hstore.rb +109 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/inet.rb +15 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/interval.rb +49 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/jsonb.rb +15 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/legacy_point.rb +44 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/macaddr.rb +25 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/money.rb +41 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/oid.rb +15 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/point.rb +64 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/range.rb +115 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/specialized_string.rb +18 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/timestamp.rb +15 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/timestamp_with_time_zone.rb +30 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/type_map_initializer.rb +125 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/uuid.rb +35 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/vector.rb +28 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid/xml.rb +30 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/oid.rb +38 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/quoting.rb +231 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/referential_integrity.rb +77 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/schema_creation.rb +100 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/schema_definitions.rb +243 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/schema_dumper.rb +74 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/schema_statements.rb +812 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/type_metadata.rb +44 -0
- data/lib/active_record/connection_adapters/cipherstash_pg/utils.rb +80 -0
- data/lib/active_record/connection_adapters/cipherstash_pg_adapter.rb +1100 -0
- data/lib/active_record/connection_adapters/postgres_cipherstash_adapter.rb +13 -0
- data/lib/activerecord-cipherstash-pg-adapter.rb +33 -0
- data/lib/version.rb +3 -0
- 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
data/.tool-versions
ADDED
data/CHANGELOG.md
ADDED
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,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
|