active_record_pgnats 1.0.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/LICENSE +21 -0
- data/README.md +201 -0
- data/lib/active_record_pgnats/adapters/postgres/connection.rb +21 -0
- data/lib/active_record_pgnats/adapters/postgres/query_executor.rb +29 -0
- data/lib/active_record_pgnats/adapters/postgres/servers.rb +31 -0
- data/lib/active_record_pgnats/adapters/postgres/subscriptions.rb +26 -0
- data/lib/active_record_pgnats/adapters/postgres.rb +120 -0
- data/lib/active_record_pgnats/command_recorder.rb +71 -0
- data/lib/active_record_pgnats/configuration.rb +12 -0
- data/lib/active_record_pgnats/publish_trigger.rb +64 -0
- data/lib/active_record_pgnats/schema_dumper.rb +36 -0
- data/lib/active_record_pgnats/server.rb +48 -0
- data/lib/active_record_pgnats/statements.rb +122 -0
- data/lib/active_record_pgnats/subscription.rb +31 -0
- data/lib/active_record_pgnats/version.rb +5 -0
- data/lib/active_record_pgnats.rb +50 -0
- metadata +120 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1d170a81eb308b003ad5a6c3eed99da1101d584bf46249cf66a458c6057a8641
|
|
4
|
+
data.tar.gz: e16809da5ed462c0af895d6fcc1fba5215f1ff0f8947646290c47ca24ff321bc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '009a011ba3566c2a05af7f10a6494eebc3f3213cb55d1dba0beb75129d154b6c08bef87c3f9cfbf092db92ef672581d19704cc0cbe351d92cca4a87311e07c8e'
|
|
7
|
+
data.tar.gz: b37d38071d69ff6dbd8dac221e74b301b86edccdf30bf422f5b0ec231d06fdd203368321d590bed3ba3e5a53bf6ac7452b86a229a8b5d2605a9d5adbb71f3d3a
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nathan K
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# active_record_pgnats
|
|
2
|
+
|
|
3
|
+
Active Record migrations for [pgnats](https://github.com/luxms/pgnats), the
|
|
4
|
+
Postgres extension that publishes to and subscribes from NATS.
|
|
5
|
+
|
|
6
|
+
Modelled on [F(x)](https://github.com/teoljungberg/fx) and
|
|
7
|
+
[pg_cron-rails](https://github.com/n-at-han-k/pg_cron-rails): migration
|
|
8
|
+
statements, reversibility through the command recorder, and a schema dumper so
|
|
9
|
+
`schema.rb` carries what the extension holds.
|
|
10
|
+
|
|
11
|
+
Two gems in this repository, sharing one `lib/` tree:
|
|
12
|
+
|
|
13
|
+
| gem | what it is |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `active_record_pgnats` | the statements, the adapter and the dumper. Active Record only — no `railties`, no `Rails.root`, no railtie |
|
|
16
|
+
| `active_record_pgnats-rails` | a railtie installing the hooks at the `on_load :active_record` moment, and generators for the migrations |
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# Gemfile -- Active Record on its own
|
|
20
|
+
gem "active_record_pgnats"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require "active_record_pgnats" # installs the statements, recorder and dumper
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# Gemfile -- Rails
|
|
29
|
+
gem "active_record_pgnats-rails"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Under Rails the core defers: requiring it does nothing until the railtie runs
|
|
33
|
+
the install, because Active Record may not be loaded when the bundle is. Use the
|
|
34
|
+
`-rails` gem there rather than the core on its own.
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
rails generate active_record_pgnats:server nats --host=127.0.0.1 --port=4222
|
|
38
|
+
rails generate active_record_pgnats:subscription events.user.created handle_user_created
|
|
39
|
+
rails generate active_record_pgnats:publish_trigger maps_devices --subject="devices.%s.%s"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
pgnats itself has to be loaded by the server (`shared_preload_libraries =
|
|
43
|
+
'pgnats'`) and enabled in the database. That part is Active Record's own:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
enable_extension "pgnats"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Every statement below is a no-op when the extension is not installed, so a
|
|
50
|
+
migration still runs against a database without pgnats instead of needing its
|
|
51
|
+
own guard.
|
|
52
|
+
|
|
53
|
+
## The server
|
|
54
|
+
|
|
55
|
+
pgnats keeps its entire configuration on a foreign server, and allows exactly
|
|
56
|
+
one per database — its own event trigger raises on a second.
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
class ConfigureNats < ActiveRecord::Migration[7.2]
|
|
60
|
+
def change
|
|
61
|
+
create_nats_server :nats, host: "127.0.0.1", port: 4222
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Options: `host`, `port`, `capacity`, `tls_ca_path`, `tls_cert_path`,
|
|
67
|
+
`tls_key_path`, `notify_subject`, `patroni_url`. Anything else raises — the
|
|
68
|
+
wrapper's validator accepts a typo that pgnats then ignores, which is a bad way
|
|
69
|
+
to find out. Values are cast to text, because every FDW option is text.
|
|
70
|
+
|
|
71
|
+
Changing them is declarative: the hash is the desired state, and an option left
|
|
72
|
+
out is dropped.
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
update_nats_server :nats,
|
|
76
|
+
host: "127.0.0.1", capacity: 256,
|
|
77
|
+
revert_to: {host: "127.0.0.1"}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The adapter diffs against the catalog to pick `SET` or `ADD` per option, since
|
|
81
|
+
`ALTER SERVER` rejects the wrong one, and follows every change with
|
|
82
|
+
`pgnats_reload_conf()` so the running background worker picks it up instead of
|
|
83
|
+
waiting for its next connect.
|
|
84
|
+
|
|
85
|
+
`revert_to:` is what makes `update_nats_server` and `drop_nats_server`
|
|
86
|
+
reversible. Without it a rollback raises `IrreversibleMigration` rather than
|
|
87
|
+
leaving the database unconfigured.
|
|
88
|
+
|
|
89
|
+
## Subscriptions
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
create_nats_subscription "events.user.created", function: "handle_user_created"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The function has to exist already and must take a single `bytea` — the message
|
|
96
|
+
payload. Create it in an earlier migration; F(x) coexists with this gem and is
|
|
97
|
+
the obvious way. A subscription needs no `revert_to`: the subject and the
|
|
98
|
+
function are the whole of it.
|
|
99
|
+
|
|
100
|
+
Note what this buys you: a core NATS subscription, so no replay and no acks.
|
|
101
|
+
Messages published while the database is down are gone.
|
|
102
|
+
|
|
103
|
+
## Publishing row changes
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
create_nats_publish_trigger :maps_devices, subject: "pg_changes.%s.%s"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Writes a trigger function and an `AFTER INSERT OR UPDATE OR DELETE` trigger,
|
|
110
|
+
both named `nats_publish_<table>`. `subject` is a format string given the table
|
|
111
|
+
name and the lowercased operation, so one definition covers all three. The
|
|
112
|
+
payload is `{table, op, at, row}`, with `row` the new tuple (the old one on a
|
|
113
|
+
delete).
|
|
114
|
+
|
|
115
|
+
The publish is wrapped in its own `BEGIN … EXCEPTION WHEN OTHERS THEN NULL`, so
|
|
116
|
+
a NATS problem can never roll back the write that fired the trigger. Put another
|
|
117
|
+
way: this is best-effort, and pgnats' JetStream publish does not wait for an
|
|
118
|
+
ack, so a subject no stream covers is dropped silently. Create the stream first.
|
|
119
|
+
|
|
120
|
+
`drop_nats_publish_trigger :maps_devices, revert_to_subject: "pg_changes.%s.%s"`
|
|
121
|
+
to go the other way.
|
|
122
|
+
|
|
123
|
+
## schema.rb
|
|
124
|
+
|
|
125
|
+
The dumper appends the server and its subscriptions after the tables:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
create_nats_server :nats, host: "127.0.0.1", port: "4222"
|
|
129
|
+
|
|
130
|
+
create_nats_subscription "events.user.created", function: "handle_user_created"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
It hooks `#tables` rather than `#extensions`, which is F(x)'s choice and is
|
|
134
|
+
load-bearing: `#extensions` is private and is redefined by the PostgreSQL
|
|
135
|
+
dumper, so a prepended module never intercepts it and the section silently never
|
|
136
|
+
appears.
|
|
137
|
+
|
|
138
|
+
Publish triggers are **not** dumped here. Once created they are an ordinary
|
|
139
|
+
function and trigger, which F(x) dumps as `create_function` / `create_trigger`
|
|
140
|
+
and `structure.sql` carries anyway; emitting them twice would make a schema load
|
|
141
|
+
fail.
|
|
142
|
+
|
|
143
|
+
## Using another connection
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
ActiveRecordPgnats.configure do |config|
|
|
147
|
+
config.adapter = ActiveRecordPgnats::Adapters::Postgres.new(SomeOtherModel)
|
|
148
|
+
end
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
It defaults to `ActiveRecord::Base`, which is almost always right: the foreign
|
|
152
|
+
server and `pgnats.subscriptions` live in the application's own database.
|
|
153
|
+
|
|
154
|
+
## Alongside F(x)
|
|
155
|
+
|
|
156
|
+
Safe to load together. Every helper here, private ones included, is prefixed
|
|
157
|
+
`nats_` / `_nats_` on purpose — both modules are included into
|
|
158
|
+
`ActiveRecord::ConnectionAdapters::AbstractAdapter`, and a shared private name
|
|
159
|
+
shadows F(x)'s copy for every migration in the application. pg_cron-rails
|
|
160
|
+
learned this the hard way: its `resolve_sql_definition` took one argument fewer
|
|
161
|
+
than F(x)'s, so every `create_function` in the host app died with
|
|
162
|
+
`ArgumentError: wrong number of arguments (given 4, expected 3)`.
|
|
163
|
+
|
|
164
|
+
## Development
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
nix develop # or direnv allow
|
|
168
|
+
bin/setup # install dependencies
|
|
169
|
+
bin/test # run the tests
|
|
170
|
+
bin/console # interactive prompt
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
bin/test # no database needed
|
|
175
|
+
DATABASE_URL=postgres:///my_db bin/test test/integration # against real pgnats
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The unit suite runs against a fake adapter. The integration spec skips itself
|
|
179
|
+
unless `DATABASE_URL` is set and pgnats is installed there.
|
|
180
|
+
|
|
181
|
+
One Gemfile covers the whole repository. Neither of its own gems is in the
|
|
182
|
+
bundle — the devshell puts `lib/` on `RUBYLIB` — so no lockfile records a
|
|
183
|
+
version and a bump has nothing to keep in step. After touching the Gemfile,
|
|
184
|
+
`nix develop --command bundix -l`.
|
|
185
|
+
|
|
186
|
+
Each gem carries its own version, changelog (`CHANGELOG-<gem>.md`) and tag
|
|
187
|
+
(`<gem>-v1.2.3`), and is released on its own schedule with
|
|
188
|
+
[gem_kit-release](https://github.com/n-at-han-k/gem_kit):
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
gem kit bump patch --gem active_record_pgnats-rails
|
|
192
|
+
gem kit changelog --write --gem active_record_pgnats-rails
|
|
193
|
+
gem kit release --gem active_record_pgnats-rails
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`active_record_pgnats-rails` derives its dependency on the core from the core's
|
|
197
|
+
own version, so a bump there does not have to be written out twice.
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "delegate"
|
|
4
|
+
|
|
5
|
+
module ActiveRecordPgnats
|
|
6
|
+
module Adapters
|
|
7
|
+
class Postgres
|
|
8
|
+
# Answers whether pgnats is installed in this database.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
class Connection < SimpleDelegator
|
|
12
|
+
# Whether the extension is there to be used.
|
|
13
|
+
#
|
|
14
|
+
# @returns [Boolean] false until CREATE EXTENSION has run, which additionally needs pgnats in shared_preload_libraries and a restart. Every caller checks first, so a migration or a dump against a database without it is a no-op rather than an error.
|
|
15
|
+
def pgnats_enabled?
|
|
16
|
+
extension_enabled?("pgnats")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordPgnats
|
|
4
|
+
module Adapters
|
|
5
|
+
class Postgres
|
|
6
|
+
# Runs a query and maps its rows to domain objects.
|
|
7
|
+
# @api private
|
|
8
|
+
class QueryExecutor
|
|
9
|
+
def self.call(...)
|
|
10
|
+
new(...).call
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(connection:, query:, model_class:)
|
|
14
|
+
@connection = connection
|
|
15
|
+
@query = query
|
|
16
|
+
@model_class = model_class
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
connection.execute(query).map { |row| model_class.new(row) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
attr_reader :connection, :query, :model_class
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record_pgnats/server"
|
|
4
|
+
require "active_record_pgnats/adapters/postgres/query_executor"
|
|
5
|
+
|
|
6
|
+
module ActiveRecordPgnats
|
|
7
|
+
module Adapters
|
|
8
|
+
class Postgres
|
|
9
|
+
# Fetches pgnats' foreign server from the connection.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
class Servers
|
|
13
|
+
# Selected by wrapper, not by name: the name is the migration's to choose, and pgnats itself joins pg_foreign_data_wrapper on fdwname to find it.
|
|
14
|
+
SERVERS_QUERY = <<~SQL.freeze
|
|
15
|
+
SELECT
|
|
16
|
+
s.srvname AS name,
|
|
17
|
+
s.srvoptions AS options
|
|
18
|
+
FROM pg_foreign_server s
|
|
19
|
+
JOIN pg_foreign_data_wrapper w
|
|
20
|
+
ON w.oid = s.srvfdw
|
|
21
|
+
WHERE w.fdwname = 'pgnats_fdw'
|
|
22
|
+
ORDER BY s.srvname;
|
|
23
|
+
SQL
|
|
24
|
+
|
|
25
|
+
def self.all(connection)
|
|
26
|
+
QueryExecutor.call(connection: connection, query: SERVERS_QUERY, model_class: Server)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record_pgnats/subscription"
|
|
4
|
+
require "active_record_pgnats/adapters/postgres/query_executor"
|
|
5
|
+
|
|
6
|
+
module ActiveRecordPgnats
|
|
7
|
+
module Adapters
|
|
8
|
+
class Postgres
|
|
9
|
+
# Fetches subscriptions from pgnats' own table.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
class Subscriptions
|
|
13
|
+
# pgnats.subscriptions is the durable state its worker reads on start, so there is no catalog view to read instead.
|
|
14
|
+
SUBSCRIPTIONS_QUERY = <<~SQL.freeze
|
|
15
|
+
SELECT subject, callback
|
|
16
|
+
FROM pgnats.subscriptions
|
|
17
|
+
ORDER BY subject, callback;
|
|
18
|
+
SQL
|
|
19
|
+
|
|
20
|
+
def self.all(connection)
|
|
21
|
+
QueryExecutor.call(connection: connection, query: SUBSCRIPTIONS_QUERY, model_class: Subscription)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record_pgnats/adapters/postgres/connection"
|
|
4
|
+
require "active_record_pgnats/adapters/postgres/servers"
|
|
5
|
+
require "active_record_pgnats/adapters/postgres/subscriptions"
|
|
6
|
+
require "active_record_pgnats/publish_trigger"
|
|
7
|
+
|
|
8
|
+
module ActiveRecordPgnats
|
|
9
|
+
module Adapters
|
|
10
|
+
# The Postgres adapter, the only engine pgnats has.
|
|
11
|
+
class Postgres
|
|
12
|
+
def initialize(connectable = ActiveRecord::Base)
|
|
13
|
+
@connectable = connectable
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def pgnats_enabled?
|
|
17
|
+
connection.pgnats_enabled?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [Array<ActiveRecordPgnats::Server>]
|
|
21
|
+
def servers
|
|
22
|
+
if pgnats_enabled?
|
|
23
|
+
Servers.all(connection)
|
|
24
|
+
else
|
|
25
|
+
[]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Array<ActiveRecordPgnats::Subscription>]
|
|
30
|
+
def subscriptions
|
|
31
|
+
if pgnats_enabled?
|
|
32
|
+
Subscriptions.all(connection)
|
|
33
|
+
else
|
|
34
|
+
[]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def create_server(name, options)
|
|
39
|
+
execute(<<~SQL)
|
|
40
|
+
CREATE SERVER #{quote_name(name)} FOREIGN DATA WRAPPER pgnats_fdw
|
|
41
|
+
OPTIONS (#{options.map { |key, value| "#{key} #{quote(value)}" }.join(", ")});
|
|
42
|
+
SQL
|
|
43
|
+
|
|
44
|
+
reload_conf
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Moves the server's options to the given set.
|
|
48
|
+
#
|
|
49
|
+
# @parameter name [String | Symbol] the server to alter.
|
|
50
|
+
# @parameter options [Hash] the desired state, not a patch: anything absent is dropped.
|
|
51
|
+
# @returns [void]
|
|
52
|
+
def update_server(name, options)
|
|
53
|
+
# ALTER SERVER takes SET for an option already present and ADD for one that is not, and rejects the wrong verb, so the current options are read back and diffed.
|
|
54
|
+
current = server_options(name)
|
|
55
|
+
|
|
56
|
+
changes = options.map { |key, value| "#{current.key?(key) ? "SET" : "ADD"} #{key} #{quote(value)}" }
|
|
57
|
+
changes += (current.keys - options.keys).map { |key| "DROP #{key}" }
|
|
58
|
+
|
|
59
|
+
if changes.empty?
|
|
60
|
+
nil
|
|
61
|
+
else
|
|
62
|
+
execute("ALTER SERVER #{quote_name(name)} OPTIONS (#{changes.join(", ")});")
|
|
63
|
+
reload_conf
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def drop_server(name)
|
|
68
|
+
execute("DROP SERVER #{quote_name(name)};")
|
|
69
|
+
reload_conf
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def create_subscription(subject, function)
|
|
73
|
+
execute("SELECT nats_subscribe(#{quote(subject)}, #{quote(function)}::regproc);")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def drop_subscription(subject, function)
|
|
77
|
+
execute("SELECT nats_unsubscribe(#{quote(subject)}, #{quote(function)}::regproc);")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def create_publish_trigger(table, subject)
|
|
81
|
+
execute(PublishTrigger.sql(table: table, subject: subject))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def drop_publish_trigger(table)
|
|
85
|
+
execute(PublishTrigger.drop_sql(table: table))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Tells the background worker to re-read the options.
|
|
89
|
+
#
|
|
90
|
+
# @returns [void] without this the new configuration only takes effect on the next connect.
|
|
91
|
+
def reload_conf
|
|
92
|
+
execute("SELECT pgnats_reload_conf();")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
attr_reader :connectable
|
|
98
|
+
|
|
99
|
+
def connection
|
|
100
|
+
Connection.new(connectable.connection)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def execute(sql)
|
|
104
|
+
connection.execute(sql)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def server_options(name)
|
|
108
|
+
servers.find { |server| server.name == name.to_s }&.options || {}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def quote(value)
|
|
112
|
+
connection.quote(value.to_s)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def quote_name(name)
|
|
116
|
+
connection.quote_table_name(name)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordPgnats
|
|
4
|
+
# Makes the statements reversible.
|
|
5
|
+
module CommandRecorder
|
|
6
|
+
%i[
|
|
7
|
+
create_nats_server update_nats_server drop_nats_server
|
|
8
|
+
create_nats_subscription drop_nats_subscription
|
|
9
|
+
create_nats_publish_trigger drop_nats_publish_trigger
|
|
10
|
+
].each do |statement|
|
|
11
|
+
define_method(statement) do |*args, &block|
|
|
12
|
+
record(statement, args, &block)
|
|
13
|
+
end
|
|
14
|
+
ruby2_keywords statement
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Inverts a server creation.
|
|
18
|
+
#
|
|
19
|
+
# @parameter args [Array] the recorded arguments.
|
|
20
|
+
# @returns [Array] the statement to run instead, and its arguments. A subscription inverts on its own, subject plus function being the whole of it; the server does not, its options being the definition, so a migration that drops or rewrites them has to say what to go back to.
|
|
21
|
+
def invert_create_nats_server(args)
|
|
22
|
+
[:drop_nats_server, args]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def invert_drop_nats_server(args)
|
|
26
|
+
_nats_invert_server(:create_nats_server, args)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def invert_update_nats_server(args)
|
|
30
|
+
_nats_invert_server(:update_nats_server, args)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def invert_create_nats_subscription(args)
|
|
34
|
+
[:drop_nats_subscription, args]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def invert_drop_nats_subscription(args)
|
|
38
|
+
[:create_nats_subscription, args]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def invert_create_nats_publish_trigger(args)
|
|
42
|
+
[:drop_nats_publish_trigger, [args.first]]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def invert_drop_nats_publish_trigger(args)
|
|
46
|
+
name, options = args
|
|
47
|
+
subject = (options || {})[:revert_to_subject]
|
|
48
|
+
|
|
49
|
+
unless subject
|
|
50
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
51
|
+
"`drop_nats_publish_trigger` is reversible only if given a `revert_to_subject`"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
[:create_nats_publish_trigger, [name, Hash.ruby2_keywords_hash({subject: subject})]]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def _nats_invert_server(method, args)
|
|
60
|
+
name, options = args
|
|
61
|
+
revert_to = (options || {})[:revert_to]
|
|
62
|
+
|
|
63
|
+
unless revert_to
|
|
64
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
65
|
+
"`#{method}` is reversible only if given a `revert_to`"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
[method, [name, Hash.ruby2_keywords_hash(revert_to.to_h)]]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordPgnats
|
|
4
|
+
class Configuration
|
|
5
|
+
# The adapter statements run through. Defaults to the Postgres adapter over ActiveRecord::Base's own connection: pgnats' foreign server and its subscriptions table live in the application's database, so that is the connection that can see them.
|
|
6
|
+
attr_accessor :adapter
|
|
7
|
+
|
|
8
|
+
def initialize(adapter: Adapters::Postgres.new)
|
|
9
|
+
@adapter = adapter
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordPgnats
|
|
4
|
+
# Builds the SQL for a publishing trigger.
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
7
|
+
module PublishTrigger
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def name_for(table)
|
|
11
|
+
"nats_publish_#{table}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def sql(table:, subject:, name: nil)
|
|
15
|
+
name ||= name_for(table)
|
|
16
|
+
|
|
17
|
+
<<~SQL
|
|
18
|
+
CREATE OR REPLACE FUNCTION #{name}() RETURNS trigger
|
|
19
|
+
LANGUAGE plpgsql AS $pgnats$
|
|
20
|
+
DECLARE
|
|
21
|
+
payload jsonb;
|
|
22
|
+
BEGIN
|
|
23
|
+
IF TG_OP = 'DELETE' THEN
|
|
24
|
+
payload := to_jsonb(OLD);
|
|
25
|
+
ELSE
|
|
26
|
+
payload := to_jsonb(NEW);
|
|
27
|
+
END IF;
|
|
28
|
+
|
|
29
|
+
-- A NATS problem must never roll back the write that fired the trigger.
|
|
30
|
+
BEGIN
|
|
31
|
+
PERFORM nats_publish_json_stream(
|
|
32
|
+
format(#{quote(subject)}, TG_TABLE_NAME, lower(TG_OP)),
|
|
33
|
+
json_build_object('table', TG_TABLE_NAME, 'op', TG_OP, 'at', now(), 'row', payload)
|
|
34
|
+
);
|
|
35
|
+
EXCEPTION WHEN OTHERS THEN
|
|
36
|
+
NULL;
|
|
37
|
+
END;
|
|
38
|
+
|
|
39
|
+
RETURN NULL;
|
|
40
|
+
END
|
|
41
|
+
$pgnats$;
|
|
42
|
+
|
|
43
|
+
DROP TRIGGER IF EXISTS #{name} ON #{table};
|
|
44
|
+
|
|
45
|
+
CREATE TRIGGER #{name}
|
|
46
|
+
AFTER INSERT OR UPDATE OR DELETE ON #{table}
|
|
47
|
+
FOR EACH ROW EXECUTE FUNCTION #{name}();
|
|
48
|
+
SQL
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def drop_sql(table:, name: nil)
|
|
52
|
+
name ||= name_for(table)
|
|
53
|
+
|
|
54
|
+
<<~SQL
|
|
55
|
+
DROP TRIGGER IF EXISTS #{name} ON #{table};
|
|
56
|
+
DROP FUNCTION IF EXISTS #{name}();
|
|
57
|
+
SQL
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def quote(value)
|
|
61
|
+
"'#{value.to_s.gsub("'", "''")}'"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordPgnats
|
|
4
|
+
# Writes the server and its subscriptions into schema.rb.
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
7
|
+
module SchemaDumper
|
|
8
|
+
# Appends the pgnats section to the schema.
|
|
9
|
+
#
|
|
10
|
+
# @parameter stream [IO] the schema being written.
|
|
11
|
+
# @returns [void] #tables is the hook, as it is in F(x) and for the same reason: #extensions is private and is redefined by the PostgreSQL-specific dumper, so a prepended module never intercepts it and the dump comes out silently missing its pgnats section.
|
|
12
|
+
def tables(stream)
|
|
13
|
+
# After super, because a subscription names a function that a statement above it creates.
|
|
14
|
+
super
|
|
15
|
+
|
|
16
|
+
nats_servers(stream)
|
|
17
|
+
nats_subscriptions(stream)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def nats_servers(stream)
|
|
23
|
+
ActiveRecordPgnats.database.servers.each do |server|
|
|
24
|
+
stream.puts
|
|
25
|
+
stream.puts(server.to_schema)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def nats_subscriptions(stream)
|
|
30
|
+
ActiveRecordPgnats.database.subscriptions.each do |subscription|
|
|
31
|
+
stream.puts
|
|
32
|
+
stream.puts(subscription.to_schema)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordPgnats
|
|
4
|
+
# pgnats' foreign server, as pg_foreign_server holds it.
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
7
|
+
class Server
|
|
8
|
+
include Comparable
|
|
9
|
+
|
|
10
|
+
attr_reader :name, :options
|
|
11
|
+
|
|
12
|
+
def initialize(row)
|
|
13
|
+
@name = row.fetch("name")
|
|
14
|
+
@options = self.class.parse_options(row.fetch("options"))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Parses srvoptions into a hash.
|
|
18
|
+
#
|
|
19
|
+
# @parameter value [Array | String] a Postgres text[] of "key=value" items, which the pg adapter hands over either as an Array or as the raw "{a=b,c=d}" literal.
|
|
20
|
+
# @returns [Hash] option name => value.
|
|
21
|
+
def self.parse_options(value)
|
|
22
|
+
if value.is_a?(Array)
|
|
23
|
+
items = value
|
|
24
|
+
else
|
|
25
|
+
items = value.to_s.delete_prefix("{").delete_suffix("}").split(",")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
items.to_h do |item|
|
|
29
|
+
key, _, rest = item.to_s.partition("=")
|
|
30
|
+
[key, rest]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def <=>(other)
|
|
35
|
+
name <=> other.name
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def ==(other)
|
|
39
|
+
name == other.name && options == other.options
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_schema
|
|
43
|
+
<<~SCHEMA.then { |schema| schema.gsub(/^(?=.)/, " ") }
|
|
44
|
+
create_nats_server :#{name}, #{options.map { |key, value| "#{key}: #{value.inspect}" }.join(", ")}
|
|
45
|
+
SCHEMA
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordPgnats
|
|
4
|
+
# The migration DSL for pgnats.
|
|
5
|
+
module Statements
|
|
6
|
+
# The options pgnats reads off its foreign server.
|
|
7
|
+
SERVER_OPTIONS = %i[
|
|
8
|
+
host port capacity tls_ca_path tls_cert_path tls_key_path notify_subject patroni_url
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
# Creates pgnats' foreign server, its whole configuration.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# create_nats_server :nats, host: "127.0.0.1", port: 4222
|
|
15
|
+
#
|
|
16
|
+
# @parameter name [String | Symbol] the server's name, the migration's to choose.
|
|
17
|
+
# @parameter revert_to [Hash] unused here; the command recorder reads it when inverting.
|
|
18
|
+
# @parameter options [Hash] any of {SERVER_OPTIONS}.
|
|
19
|
+
# @returns [void] nothing happens at all when pgnats is not installed in this database.
|
|
20
|
+
def create_nats_server(name, revert_to: nil, **options)
|
|
21
|
+
# pgnats permits exactly one server on the pgnats_fdw wrapper per database -- its own event trigger raises on a second -- so later changes go through #update_nats_server.
|
|
22
|
+
if ActiveRecordPgnats.database.pgnats_enabled?
|
|
23
|
+
ActiveRecordPgnats.database.create_server(name, _nats_validated_options(options))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Changes the server's options to exactly those given.
|
|
28
|
+
#
|
|
29
|
+
# @parameter name [String | Symbol] the server to alter.
|
|
30
|
+
# @parameter revert_to [Hash] the options to restore on rollback; without it the migration is irreversible.
|
|
31
|
+
# @parameter options [Hash] the options the server should end up with. Declarative, not additive: anything left out is dropped.
|
|
32
|
+
# @returns [void]
|
|
33
|
+
def update_nats_server(name, revert_to: nil, **options)
|
|
34
|
+
if ActiveRecordPgnats.database.pgnats_enabled?
|
|
35
|
+
ActiveRecordPgnats.database.update_server(name, _nats_validated_options(options))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Drops the server, leaving pgnats unconfigured.
|
|
40
|
+
#
|
|
41
|
+
# @parameter name [String | Symbol] the server to drop.
|
|
42
|
+
# @parameter revert_to [Hash] the options to recreate it with on rollback; without it the migration is irreversible.
|
|
43
|
+
# @returns [void]
|
|
44
|
+
def drop_nats_server(name, revert_to: nil)
|
|
45
|
+
if ActiveRecordPgnats.database.pgnats_enabled?
|
|
46
|
+
ActiveRecordPgnats.database.drop_server(name)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Subscribes a Postgres function to a NATS subject.
|
|
51
|
+
#
|
|
52
|
+
# @example
|
|
53
|
+
# create_nats_subscription "events.user.created", function: "handle_user_created"
|
|
54
|
+
#
|
|
55
|
+
# @parameter subject [String] the NATS subject to subscribe to.
|
|
56
|
+
# @parameter function [String | Symbol] the function the background worker calls. It must exist already and take a single bytea, the message payload.
|
|
57
|
+
# @returns [void]
|
|
58
|
+
def create_nats_subscription(subject, function:)
|
|
59
|
+
# nats_subscribe resolves the name to an OID, so create the function in an earlier migration -- F(x) is the obvious way -- or this fails.
|
|
60
|
+
if ActiveRecordPgnats.database.pgnats_enabled?
|
|
61
|
+
ActiveRecordPgnats.database.create_subscription(subject, function)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Unsubscribes the function from the subject.
|
|
66
|
+
#
|
|
67
|
+
# @parameter subject [String] the subject it was subscribed to.
|
|
68
|
+
# @parameter function [String | Symbol] the function to stop calling.
|
|
69
|
+
# @returns [void]
|
|
70
|
+
def drop_nats_subscription(subject, function:)
|
|
71
|
+
if ActiveRecordPgnats.database.pgnats_enabled?
|
|
72
|
+
ActiveRecordPgnats.database.drop_subscription(subject, function)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Publishes every row change on a table.
|
|
77
|
+
#
|
|
78
|
+
# @example
|
|
79
|
+
# create_nats_publish_trigger :maps_devices, subject: "pg_changes.%s.%s"
|
|
80
|
+
#
|
|
81
|
+
# @parameter table [String | Symbol] the table to watch.
|
|
82
|
+
# @parameter subject [String] a format string given the table name and the lowercased operation, so one definition covers insert, update and delete.
|
|
83
|
+
# @parameter revert_to_subject [String] unused here; the command recorder reads it when inverting a drop.
|
|
84
|
+
# @returns [void]
|
|
85
|
+
def create_nats_publish_trigger(table, subject:, revert_to_subject: nil)
|
|
86
|
+
# The function and the trigger are ordinary ones once created, so the schema dumper leaves them alone: F(x) dumps them, and structure.sql carries them either way.
|
|
87
|
+
if ActiveRecordPgnats.database.pgnats_enabled?
|
|
88
|
+
ActiveRecordPgnats.database.create_publish_trigger(table, subject)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Drops the publish trigger and its function.
|
|
93
|
+
#
|
|
94
|
+
# @parameter table [String | Symbol] the table it watches.
|
|
95
|
+
# @parameter revert_to_subject [String] the subject to recreate it with on rollback; without it the migration is irreversible.
|
|
96
|
+
# @returns [void]
|
|
97
|
+
def drop_nats_publish_trigger(table, revert_to_subject: nil)
|
|
98
|
+
if ActiveRecordPgnats.database.pgnats_enabled?
|
|
99
|
+
ActiveRecordPgnats.database.drop_publish_trigger(table)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# Rejects unknown options and stringifies the rest.
|
|
106
|
+
#
|
|
107
|
+
# @parameter options [Hash] the options as the migration wrote them.
|
|
108
|
+
# @returns [Hash] the same options with keys and values as strings, because every FDW option is text.
|
|
109
|
+
# @raises [ArgumentError] when an option is not one pgnats reads: the wrapper's validator accepts a typo that pgnats then ignores.
|
|
110
|
+
def _nats_validated_options(options)
|
|
111
|
+
unknown = options.keys.map(&:to_sym) - SERVER_OPTIONS
|
|
112
|
+
|
|
113
|
+
unless unknown.empty?
|
|
114
|
+
message = "unknown pgnats server option(s): #{unknown.join(", ")}. Known options: #{SERVER_OPTIONS.join(", ")}"
|
|
115
|
+
|
|
116
|
+
raise ArgumentError, message, caller
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
options.to_h { |key, value| [key.to_s, value.to_s] }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecordPgnats
|
|
4
|
+
# A row of pgnats.subscriptions: a subject and its function.
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
7
|
+
class Subscription
|
|
8
|
+
include Comparable
|
|
9
|
+
|
|
10
|
+
attr_reader :subject, :function
|
|
11
|
+
|
|
12
|
+
def initialize(row)
|
|
13
|
+
@subject = row.fetch("subject")
|
|
14
|
+
@function = row.fetch("callback")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def <=>(other)
|
|
18
|
+
[subject, function] <=> [other.subject, other.function]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def ==(other)
|
|
22
|
+
subject == other.subject && function == other.function
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_schema
|
|
26
|
+
<<~SCHEMA.then { |schema| schema.gsub(/^(?=.)/, " ") }
|
|
27
|
+
create_nats_subscription #{subject.inspect}, function: #{function.inspect}
|
|
28
|
+
SCHEMA
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
|
|
5
|
+
require "active_record_pgnats/version"
|
|
6
|
+
require "active_record_pgnats/adapters/postgres"
|
|
7
|
+
require "active_record_pgnats/command_recorder"
|
|
8
|
+
require "active_record_pgnats/configuration"
|
|
9
|
+
require "active_record_pgnats/publish_trigger"
|
|
10
|
+
require "active_record_pgnats/schema_dumper"
|
|
11
|
+
require "active_record_pgnats/server"
|
|
12
|
+
require "active_record_pgnats/statements"
|
|
13
|
+
require "active_record_pgnats/subscription"
|
|
14
|
+
|
|
15
|
+
# Migration methods for pgnats, Active Record only.
|
|
16
|
+
module ActiveRecordPgnats
|
|
17
|
+
class << self
|
|
18
|
+
# Installs the statements, the recorder and the dumper.
|
|
19
|
+
#
|
|
20
|
+
# @returns [Boolean] true the first time, false once the hooks are in place.
|
|
21
|
+
def load
|
|
22
|
+
if @loaded
|
|
23
|
+
false
|
|
24
|
+
else
|
|
25
|
+
ActiveRecord::ConnectionAdapters::AbstractAdapter.include(Statements)
|
|
26
|
+
ActiveRecord::Migration::CommandRecorder.include(CommandRecorder)
|
|
27
|
+
ActiveRecord::SchemaDumper.prepend(SchemaDumper)
|
|
28
|
+
@loaded = true
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def configuration
|
|
33
|
+
@configuration ||= Configuration.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attr_writer :configuration
|
|
37
|
+
|
|
38
|
+
def configure
|
|
39
|
+
yield configuration
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def database
|
|
43
|
+
configuration.adapter
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
unless defined?(::Rails::Railtie)
|
|
49
|
+
ActiveRecordPgnats.load
|
|
50
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: active_record_pgnats
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nathan K
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-01 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activerecord
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.2'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: pg
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.5'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.5'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '13.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rspec
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.13'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.13'
|
|
68
|
+
description: |
|
|
69
|
+
Adds methods to ActiveRecord::Migration to manage pgnats: its foreign server,
|
|
70
|
+
its subject subscriptions, and triggers that publish row changes to NATS.
|
|
71
|
+
Active Record only -- Rails support lives in the sibling
|
|
72
|
+
active_record_pgnats-rails gem.
|
|
73
|
+
email:
|
|
74
|
+
- nathankidd@hey.com
|
|
75
|
+
executables: []
|
|
76
|
+
extensions: []
|
|
77
|
+
extra_rdoc_files: []
|
|
78
|
+
files:
|
|
79
|
+
- LICENSE
|
|
80
|
+
- README.md
|
|
81
|
+
- lib/active_record_pgnats.rb
|
|
82
|
+
- lib/active_record_pgnats/adapters/postgres.rb
|
|
83
|
+
- lib/active_record_pgnats/adapters/postgres/connection.rb
|
|
84
|
+
- lib/active_record_pgnats/adapters/postgres/query_executor.rb
|
|
85
|
+
- lib/active_record_pgnats/adapters/postgres/servers.rb
|
|
86
|
+
- lib/active_record_pgnats/adapters/postgres/subscriptions.rb
|
|
87
|
+
- lib/active_record_pgnats/command_recorder.rb
|
|
88
|
+
- lib/active_record_pgnats/configuration.rb
|
|
89
|
+
- lib/active_record_pgnats/publish_trigger.rb
|
|
90
|
+
- lib/active_record_pgnats/schema_dumper.rb
|
|
91
|
+
- lib/active_record_pgnats/server.rb
|
|
92
|
+
- lib/active_record_pgnats/statements.rb
|
|
93
|
+
- lib/active_record_pgnats/subscription.rb
|
|
94
|
+
- lib/active_record_pgnats/version.rb
|
|
95
|
+
homepage: https://github.com/n-at-han-k/active-record-pgnats
|
|
96
|
+
licenses:
|
|
97
|
+
- MIT
|
|
98
|
+
metadata:
|
|
99
|
+
homepage_uri: https://github.com/n-at-han-k/active-record-pgnats
|
|
100
|
+
source_code_uri: https://github.com/n-at-han-k/active-record-pgnats
|
|
101
|
+
changelog_uri: https://github.com/n-at-han-k/active-record-pgnats/blob/main/CHANGELOG-active_record_pgnats.md
|
|
102
|
+
rubygems_mfa_required: 'true'
|
|
103
|
+
rdoc_options: []
|
|
104
|
+
require_paths:
|
|
105
|
+
- lib
|
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: 3.1.0
|
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '0'
|
|
116
|
+
requirements: []
|
|
117
|
+
rubygems_version: 3.7.2
|
|
118
|
+
specification_version: 4
|
|
119
|
+
summary: Active Record migrations for the pgnats extension.
|
|
120
|
+
test_files: []
|