changebase 0.1 → 0.2
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 +4 -4
- data/README.md +74 -9
- data/lib/changebase/action_controller.rb +11 -11
- data/lib/changebase/active_record.rb +10 -93
- data/lib/changebase/connection.rb +399 -0
- data/lib/changebase/inline/active_record.rb +161 -0
- data/lib/changebase/inline/event.rb +25 -0
- data/lib/changebase/inline/transaction.rb +71 -0
- data/lib/changebase/inline.rb +32 -0
- data/lib/changebase/railtie.rb +18 -10
- data/lib/changebase/replication/active_record.rb +96 -0
- data/lib/changebase/replication.rb +24 -0
- data/lib/changebase/version.rb +1 -1
- data/lib/changebase.rb +58 -7
- metadata +29 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cb5026dc3675467dc638006f5953c5ba3a1ec4f486e63ecd4066fd2a027356f2
|
4
|
+
data.tar.gz: e4d58239417908fec5572dfe5e99bfb6d48db046679c39621d632cdcba8951fc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ed3fa5539602e0417635ff5333ec804a749930db38802a6f56636363b91a9896b1aa7acb18c0665471e9f66863e54e391a644d34e016014dda5090247eed488
|
7
|
+
data.tar.gz: 0cae8f40c3bc961c6381c0e4b9773b19d408e3602cbf47340d77479cc34c0bbb7827726f030b5929d3e466c944372b8f16d1792c0390f3f7696508ed88a53dd4
|
data/README.md
CHANGED
@@ -47,27 +47,27 @@ Below are several diffent way of including metadata:
|
|
47
47
|
|
48
48
|
```ruby
|
49
49
|
class ApplicationController < ActionController::Base
|
50
|
-
|
50
|
+
|
51
51
|
# Just a block returning a hash of metadata.
|
52
52
|
changebase do
|
53
53
|
{ my: data }
|
54
54
|
end
|
55
|
-
|
55
|
+
|
56
56
|
# Sets `release` in the metadata to the string RELEASE_SHA
|
57
57
|
changebase :release, RELEASE_SHA
|
58
|
-
|
58
|
+
|
59
59
|
# Sets `request_id` in the metadata to the value returned from the `Proc`
|
60
60
|
changebase :request_id, -> { request.uuid }
|
61
|
-
|
61
|
+
|
62
62
|
# Sets `user.id` in the metadata to the value returned from the
|
63
63
|
# `current_user_id` function
|
64
64
|
changebase :user, :id, :current_user_id
|
65
|
-
|
65
|
+
|
66
66
|
# Sets `user.name` in the metadata to the value returned from the block
|
67
67
|
changebase :user, :name do
|
68
68
|
current_user.name
|
69
69
|
end
|
70
|
-
|
70
|
+
|
71
71
|
def current_user_id
|
72
72
|
current_user.id
|
73
73
|
end
|
@@ -99,8 +99,15 @@ To include metadata when creating or modifying data with ActiveRecord:
|
|
99
99
|
|
100
100
|
### Configuration
|
101
101
|
|
102
|
-
|
103
|
-
|
102
|
+
#### Replication Mode
|
103
|
+
|
104
|
+
The default mode for the `changebase` gem is `replication`. In this mode
|
105
|
+
Changebase is setup to replicate your database and record events via the
|
106
|
+
replication stream.
|
107
|
+
|
108
|
+
The default configuration `changebase` will write metadata to the
|
109
|
+
`"changebase_metadata"` table. To configure the metadata table create an
|
110
|
+
initializer at `config/initializers/changebase.rb` with the following:
|
104
111
|
|
105
112
|
```ruby
|
106
113
|
Rails.application.config.tap do |config|
|
@@ -112,9 +119,67 @@ If you are not using Rails you can configure Changebase directly via:
|
|
112
119
|
|
113
120
|
```ruby
|
114
121
|
Changebase.metadata_table = "my_very_cool_custom_metadata_table"
|
122
|
+
|
123
|
+
# Or
|
124
|
+
|
125
|
+
Changebase.configure(metadata_table: "my_very_cool_custom_metadata_table")
|
126
|
+
```
|
127
|
+
|
128
|
+
#### Inline Mode
|
129
|
+
|
130
|
+
If you are unable to setup database replication you can use inline mode. Events
|
131
|
+
will be sent to through the Changebase API. You will collect roughly the same
|
132
|
+
information, but potentionally to miss events and changes in your database
|
133
|
+
if you are not careful, or if another application accesses the database directly.
|
134
|
+
|
135
|
+
##### Limitations
|
136
|
+
|
137
|
+
- Any changes made to the database by ActiveRecord that does not first
|
138
|
+
instantiate the records will not be caputred. These methods include:
|
139
|
+
- [ActiveRecord::ConnectionAdapters::DatabaseStatements#execute](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/DatabaseStatements.html#method-i-execute)
|
140
|
+
- [ActiveRecord::ConnectionAdapters::DatabaseStatements#exec_query](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/DatabaseStatements.html#method-i-exec_query)
|
141
|
+
- [ActiveRecord::ConnectionAdapters::DatabaseStatements#exec_update](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/DatabaseStatements.html#method-i-exec_update)
|
142
|
+
- [ActiveRecord::ConnectionAdapters::DatabaseStatements#exec_insert](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/DatabaseStatements.html#method-i-exec_insert)
|
143
|
+
- [ActiveRecord::ConnectionAdapters::DatabaseStatements#exec_delete](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/DatabaseStatements.html#method-i-exec_delete)
|
144
|
+
- [ActiveRecord::Persistence#delete](https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-delete)
|
145
|
+
- [ActiveRecord::Persistence#update_column](https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_column)
|
146
|
+
- [ActiveRecord::Persistence#update_columns](https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_columns)
|
147
|
+
- [ActiveRecord::Relation#touch_all](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-touch_all)
|
148
|
+
- [ActiveRecord::Relation#update_all](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-update_all)
|
149
|
+
- [ActiveRecord::Relation#delete_all](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-delete_all)
|
150
|
+
- [ActiveRecord::Relation#delete_by](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-delete_by)
|
151
|
+
- Any changes made to the database outside of the Rails application will not be
|
152
|
+
captured.
|
153
|
+
- Ordering of events will not be guaranteed. The timestamp will be used as the
|
154
|
+
LSN, which may not be in the same order of transactions / events in the database.
|
155
|
+
|
156
|
+
To configure Changebase in the `"inline"` mode create a initializer at
|
157
|
+
`config/initializers/changebase.rb` with the following:
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
Rails.application.config.tap do |config|
|
161
|
+
config.changebase.mode = "inline"
|
162
|
+
config.changebase.connection = "https://#{ ENV.fetch('CHANGEBASE_API_KEY') }@changebase.io"
|
163
|
+
end
|
164
|
+
```
|
165
|
+
|
166
|
+
If you are not using Rails you can configure Changebase directly via:
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
Changebase.configure do |config|
|
170
|
+
config.changebase.mode = "inline"
|
171
|
+
config.changebase.connection = "https://#{ ENV.fetch('CHANGEBASE_API_KEY') }@changebase.io"
|
172
|
+
end
|
173
|
+
|
174
|
+
# Or
|
175
|
+
|
176
|
+
Changebase.configure(
|
177
|
+
mode: "inline",
|
178
|
+
connection: "https://API_KEY@chanbase.io"
|
179
|
+
)
|
115
180
|
```
|
116
181
|
|
117
182
|
## Bugs
|
118
183
|
|
119
|
-
If you think you found a bug, please file a ticket on the [issue
|
184
|
+
If you think you found a bug, please file a ticket on the [issue
|
120
185
|
tracker](https://github.com/changebase-io/ruby-gem/issues).
|
@@ -4,7 +4,7 @@ module Changebase::ActionController
|
|
4
4
|
included do
|
5
5
|
prepend_around_action :changebase_metadata_wrapper
|
6
6
|
end
|
7
|
-
|
7
|
+
|
8
8
|
module ClassMethods
|
9
9
|
def changebase(*keys, &block)
|
10
10
|
method = if block
|
@@ -14,18 +14,18 @@ module Changebase::ActionController
|
|
14
14
|
else
|
15
15
|
keys.first
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
@changebase_metadata ||= []
|
19
19
|
@changebase_metadata << [keys, method]
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
def changebase_metadata
|
23
23
|
klass_metadata = if instance_variable_defined?(:@changebase_metadata)
|
24
24
|
@changebase_metadata
|
25
25
|
else
|
26
26
|
[]
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
29
|
if self.superclass.respond_to?(:changebase_metadata)
|
30
30
|
klass_metadata + self.superclass.changebase_metadata
|
31
31
|
else
|
@@ -33,21 +33,21 @@ module Changebase::ActionController
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
36
|
-
|
36
|
+
|
37
37
|
def changebase_metadata
|
38
38
|
self.class.changebase_metadata
|
39
39
|
end
|
40
|
-
|
40
|
+
|
41
41
|
def changebase_metadata_wrapper(&block)
|
42
42
|
metadata = {}
|
43
|
-
|
43
|
+
|
44
44
|
changebase_metadata.each do |keys, value|
|
45
45
|
data = metadata
|
46
46
|
keys[0...-1].each do |key|
|
47
47
|
data[key] ||= {}
|
48
48
|
data = data[key]
|
49
49
|
end
|
50
|
-
|
50
|
+
|
51
51
|
value = case value
|
52
52
|
when Symbol
|
53
53
|
self.send(value)
|
@@ -56,7 +56,7 @@ module Changebase::ActionController
|
|
56
56
|
else
|
57
57
|
value
|
58
58
|
end
|
59
|
-
|
59
|
+
|
60
60
|
if keys.last
|
61
61
|
data[keys.last] ||= value
|
62
62
|
else
|
@@ -66,7 +66,7 @@ module Changebase::ActionController
|
|
66
66
|
|
67
67
|
ActiveRecord::Base.with_metadata(metadata, &block)
|
68
68
|
end
|
69
|
-
|
69
|
+
|
70
70
|
end
|
71
71
|
|
72
|
-
ActionController::Base.include(Changebase::ActionController)
|
72
|
+
ActionController::Base.include(Changebase::ActionController)
|
@@ -1,106 +1,23 @@
|
|
1
1
|
module Changebase::ActiveRecord
|
2
2
|
extend ActiveSupport::Concern
|
3
|
-
|
4
|
-
|
3
|
+
|
4
|
+
class_methods do
|
5
5
|
def with_metadata(metadata, &block)
|
6
6
|
connection.with_metadata(metadata, &block)
|
7
7
|
end
|
8
8
|
end
|
9
|
-
|
10
|
-
def with_metadata(metadata, &block)
|
11
|
-
self.class.with_metadata(metadata, &block)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
module Changebase::ActiveRecord::Connection
|
16
9
|
|
17
10
|
def with_metadata(metadata, &block)
|
18
|
-
|
19
|
-
yield
|
20
|
-
ensure
|
21
|
-
@changebase_metadata = nil
|
22
|
-
end
|
23
|
-
|
24
|
-
end
|
25
|
-
|
26
|
-
module Changebase::ActiveRecord::PostgreSQLAdapter
|
27
|
-
|
28
|
-
def initialize(*args, **margs)
|
29
|
-
@without_changebase = false
|
30
|
-
@changebase_metadata = nil
|
31
|
-
super
|
32
|
-
end
|
33
|
-
|
34
|
-
def without_changebase
|
35
|
-
@without_changebase = true
|
36
|
-
yield
|
37
|
-
ensure
|
38
|
-
@without_changebase = false
|
39
|
-
end
|
40
|
-
|
41
|
-
def drop_database(name) # :nodoc:
|
42
|
-
without_changebase { super }
|
43
|
-
end
|
44
|
-
|
45
|
-
def drop_table(table_name, **options) # :nodoc:
|
46
|
-
without_changebase { super }
|
47
|
-
end
|
48
|
-
|
49
|
-
def create_database(name, options = {})
|
50
|
-
without_changebase { super }
|
51
|
-
end
|
52
|
-
|
53
|
-
def recreate_database(name, options = {}) # :nodoc:
|
54
|
-
without_changebase { super }
|
55
|
-
end
|
56
|
-
|
57
|
-
def execute(sql, name = nil)
|
58
|
-
if !@without_changebase && !current_transaction.open? && write_query?(sql)
|
59
|
-
transaction { super }
|
60
|
-
else
|
61
|
-
super
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def exec_query(sql, name = "SQL", binds = [], prepare: false)
|
66
|
-
if !@without_changebase && !current_transaction.open? && write_query?(sql)
|
67
|
-
transaction { super }
|
68
|
-
else
|
69
|
-
super
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
def exec_delete(sql, name = nil, binds = []) # :nodoc:
|
74
|
-
if !@without_changebase && !current_transaction.open? && write_query?(sql)
|
75
|
-
transaction { super }
|
76
|
-
else
|
77
|
-
super
|
78
|
-
end
|
11
|
+
self.class.with_metadata(metadata, &block)
|
79
12
|
end
|
80
13
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
DO UPDATE SET version = :version, data = :metadata;
|
88
|
-
SQL
|
89
|
-
|
90
|
-
log(sql, "CHANGEBASE") do
|
91
|
-
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
|
92
|
-
@connection.async_exec(sql)
|
93
|
-
end
|
94
|
-
end
|
14
|
+
module Connection
|
15
|
+
def with_metadata(metadata, &block)
|
16
|
+
@changebase_metadata = metadata
|
17
|
+
yield
|
18
|
+
ensure
|
19
|
+
@changebase_metadata = nil
|
95
20
|
end
|
96
|
-
super
|
97
21
|
end
|
98
22
|
|
99
|
-
end
|
100
|
-
|
101
|
-
require 'active_record'
|
102
|
-
ActiveRecord::Base.include(Changebase::ActiveRecord)
|
103
|
-
ActiveRecord::ConnectionAdapters::AbstractAdapter.include(Changebase::ActiveRecord::Connection)
|
104
|
-
|
105
|
-
require 'active_record/connection_adapters/postgresql_adapter'
|
106
|
-
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.include(Changebase::ActiveRecord::PostgreSQLAdapter)
|
23
|
+
end
|
@@ -0,0 +1,399 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
|
3
|
+
module Changebase
|
4
|
+
|
5
|
+
class ServerError < ::RuntimeError
|
6
|
+
end
|
7
|
+
|
8
|
+
# RuntimeErrors don't get translated by Rails into
|
9
|
+
# ActiveRecord::StatementInvalid which StandardError do. Would rather
|
10
|
+
# use StandardError, but it's usefull with Changebase to know when something
|
11
|
+
# raises a Changebase::Exception::NotFound or Forbidden
|
12
|
+
class Exception < ::RuntimeError
|
13
|
+
|
14
|
+
class UnexpectedResponse < Changebase::Exception
|
15
|
+
end
|
16
|
+
|
17
|
+
class BadRequest < Changebase::Exception
|
18
|
+
end
|
19
|
+
|
20
|
+
class Unauthorized < Changebase::Exception
|
21
|
+
end
|
22
|
+
|
23
|
+
class Forbidden < Changebase::Exception
|
24
|
+
end
|
25
|
+
|
26
|
+
class NotFound < Changebase::Exception
|
27
|
+
end
|
28
|
+
|
29
|
+
class Gone < Changebase::Exception
|
30
|
+
end
|
31
|
+
|
32
|
+
class MovedPermanently < Changebase::Exception
|
33
|
+
end
|
34
|
+
|
35
|
+
class BadGateway < Changebase::Exception
|
36
|
+
end
|
37
|
+
|
38
|
+
class ApiVersionUnsupported < Changebase::Exception
|
39
|
+
end
|
40
|
+
|
41
|
+
class ServiceUnavailable < Changebase::Exception
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
# _Changebase::Connection_ is a low-level API. It provides basic HTTP #get,
|
50
|
+
# #post, #put, and #delete calls to the an HTTP(S) Server. It can also provides
|
51
|
+
# basic error checking of responses.
|
52
|
+
class Changebase::Connection
|
53
|
+
|
54
|
+
attr_reader :api_key, :host, :port, :use_ssl
|
55
|
+
|
56
|
+
# Initialize a connection Changebase.
|
57
|
+
#
|
58
|
+
# Options:
|
59
|
+
#
|
60
|
+
# * <tt>:url</tt> - An optional url used to set the protocol, host, port,
|
61
|
+
# and api_key
|
62
|
+
# * <tt>:host</tt> - The default is to connect to 127.0.0.1.
|
63
|
+
# * <tt>:port</tt> - Defaults to 80.
|
64
|
+
# * <tt>:use_ssl</tt> - Defaults to true.
|
65
|
+
# * <tt>:api_key</tt> - An optional token to send in the `Api-Key` header
|
66
|
+
# * <tt>:user_agent</tt> - An optional string. Will be joined with other
|
67
|
+
# User-Agent info.
|
68
|
+
def initialize(config)
|
69
|
+
if config[:url]
|
70
|
+
uri = URI.parse(config.delete(:url))
|
71
|
+
config[:api_key] ||= (uri.user ? CGI.unescape(uri.user) : nil)
|
72
|
+
config[:host] ||= uri.host
|
73
|
+
config[:port] ||= uri.port
|
74
|
+
config[:use_ssl] ||= (uri.scheme == 'https')
|
75
|
+
end
|
76
|
+
|
77
|
+
[:api_key, :host, :port, :use_ssl, :user_agent].each do |key|
|
78
|
+
self.instance_variable_set(:"@#{key}", config[key])
|
79
|
+
end
|
80
|
+
|
81
|
+
@connection = Net::HTTP.new(host, port)
|
82
|
+
@connection.max_retries = 0
|
83
|
+
@connection.open_timeout = 5
|
84
|
+
@connection.read_timeout = 30
|
85
|
+
@connection.write_timeout = 5
|
86
|
+
@connection.ssl_timeout = 5
|
87
|
+
@connection.keep_alive_timeout = 30
|
88
|
+
@connection.use_ssl = use_ssl
|
89
|
+
true
|
90
|
+
end
|
91
|
+
|
92
|
+
def connect!
|
93
|
+
@connection.start
|
94
|
+
end
|
95
|
+
|
96
|
+
def active?
|
97
|
+
@connection.active?
|
98
|
+
end
|
99
|
+
|
100
|
+
def reconnect!
|
101
|
+
disconnect!
|
102
|
+
connect!
|
103
|
+
end
|
104
|
+
|
105
|
+
def disconnect!
|
106
|
+
@connection.finish if @connection.active?
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns the User-Agent of the client. Defaults to:
|
110
|
+
# "Rubygems/changebase@GEM_VERSION Ruby@RUBY_VERSION-pPATCH_LEVEL RUBY_PLATFORM"
|
111
|
+
def user_agent
|
112
|
+
[
|
113
|
+
@user_agent,
|
114
|
+
"Rubygems/changebase@#{Changebase::VERSION}",
|
115
|
+
"Ruby@#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}",
|
116
|
+
RUBY_PLATFORM
|
117
|
+
].compact.join(' ')
|
118
|
+
end
|
119
|
+
|
120
|
+
# Sends a Net::HTTPRequest to the server. The headers returned from
|
121
|
+
# Connection#request_headers are automatically added to the request.
|
122
|
+
# The appropriate error is raised if the response is not in the 200..299
|
123
|
+
# range.
|
124
|
+
#
|
125
|
+
# Paramaters::
|
126
|
+
#
|
127
|
+
# * +request+ - A Net::HTTPRequest to send to the server
|
128
|
+
# * +body+ - Optional, a String, IO Object, or a Ruby object which is
|
129
|
+
# converted into JSON and sent as the body
|
130
|
+
# * +block+ - An optional block to call with the +Net::HTTPResponse+ object.
|
131
|
+
#
|
132
|
+
# Return Value::
|
133
|
+
#
|
134
|
+
# Returns the return value of the <tt>&block</tt> if given, otherwise the
|
135
|
+
# response object (a Net::HTTPResponse)
|
136
|
+
#
|
137
|
+
# Examples:
|
138
|
+
#
|
139
|
+
# #!ruby
|
140
|
+
# connection.send_request(#<Net::HTTP::Get>) # => #<Net::HTTP::Response>
|
141
|
+
#
|
142
|
+
# connection.send_request(#<Net::HTTP::Get @path="/404">) # => raises Changebase::Exception::NotFound
|
143
|
+
#
|
144
|
+
# # this will still raise an exception if the response_code is not valid
|
145
|
+
# # and the block will not be called
|
146
|
+
# connection.send_request(#<Net::HTTP::Get>) do |response|
|
147
|
+
# # ...
|
148
|
+
# end
|
149
|
+
#
|
150
|
+
# # The following example shows how to stream a response:
|
151
|
+
# connection.send_request(#<Net::HTTP::Get>) do |response|
|
152
|
+
# response.read_body do |chunk|
|
153
|
+
# io.write(chunk)
|
154
|
+
# end
|
155
|
+
# end
|
156
|
+
def send_request(request, body=nil, &block)
|
157
|
+
request_headers.each { |k, v| request[k] = v }
|
158
|
+
request['Content-Type'] ||= 'application/json'
|
159
|
+
|
160
|
+
if body.is_a?(IO)
|
161
|
+
request['Transfer-Encoding'] = 'chunked'
|
162
|
+
request.body_stream = body
|
163
|
+
elsif body.is_a?(String)
|
164
|
+
request.body = body
|
165
|
+
elsif body
|
166
|
+
request.body = JSON.generate(body)
|
167
|
+
end
|
168
|
+
|
169
|
+
return_value = nil
|
170
|
+
begin
|
171
|
+
close_connection = false
|
172
|
+
@connection.request(request) do |response|
|
173
|
+
# if response['Deprecation-Notice']
|
174
|
+
# ActiveSupport::Deprecation.warn(response['Deprecation-Notice'])
|
175
|
+
# end
|
176
|
+
|
177
|
+
validate_response_code(response)
|
178
|
+
|
179
|
+
# Get the cookies
|
180
|
+
response.each_header do |key, value|
|
181
|
+
case key.downcase
|
182
|
+
when 'connection'
|
183
|
+
close_connection = (value == 'close')
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
if block_given?
|
188
|
+
return_value = yield(response)
|
189
|
+
else
|
190
|
+
return_value = response
|
191
|
+
end
|
192
|
+
end
|
193
|
+
@connection.finish if close_connection
|
194
|
+
end
|
195
|
+
|
196
|
+
return_value
|
197
|
+
end
|
198
|
+
|
199
|
+
# Send a GET request to +path+ via +Connection#send_request+.
|
200
|
+
# See +Connection#send_request+ for more details on how the response is
|
201
|
+
# handled.
|
202
|
+
#
|
203
|
+
# Paramaters::
|
204
|
+
#
|
205
|
+
# * +path+ - The +path+ on the server to GET to.
|
206
|
+
# * +params+ - Either a String, Hash, or Ruby Object that responds to
|
207
|
+
# #to_param. Appended on the URL as query params
|
208
|
+
# * +block+ - An optional block to call with the +Net::HTTPResponse+ object.
|
209
|
+
#
|
210
|
+
# Return Value::
|
211
|
+
#
|
212
|
+
# See +Connection#send_request+
|
213
|
+
#
|
214
|
+
# Examples:
|
215
|
+
#
|
216
|
+
# #!ruby
|
217
|
+
# connection.get('/example') # => #<Net::HTTP::Response>
|
218
|
+
#
|
219
|
+
# connection.get('/example', 'query=stuff') # => #<Net::HTTP::Response>
|
220
|
+
#
|
221
|
+
# connection.get('/example', {:query => 'stuff'}) # => #<Net::HTTP::Response>
|
222
|
+
#
|
223
|
+
# connection.get('/404') # => raises Changebase::Exception::NotFound
|
224
|
+
#
|
225
|
+
# connection.get('/act') do |response|
|
226
|
+
# # ...
|
227
|
+
# end
|
228
|
+
def get(path, params='', &block)
|
229
|
+
params ||= ''
|
230
|
+
request = Net::HTTP::Get.new(path + '?' + params.to_param)
|
231
|
+
|
232
|
+
send_request(request, nil, &block)
|
233
|
+
end
|
234
|
+
|
235
|
+
# Send a POST request to +path+ via +Connection#send_request+.
|
236
|
+
# See +Connection#send_request+ for more details on how the response is
|
237
|
+
# handled.
|
238
|
+
#
|
239
|
+
# Paramaters::
|
240
|
+
#
|
241
|
+
# * +path+ - The +path+ on the server to POST to.
|
242
|
+
# * +body+ - Optional, See +Connection#send_request+.
|
243
|
+
# * +block+ - Optional, See +Connection#send_request+
|
244
|
+
#
|
245
|
+
# Return Value::
|
246
|
+
#
|
247
|
+
# See +Connection#send_request+
|
248
|
+
#
|
249
|
+
# Examples:
|
250
|
+
#
|
251
|
+
# #!ruby
|
252
|
+
# connection.post('/example') # => #<Net::HTTP::Response>
|
253
|
+
#
|
254
|
+
# connection.post('/example', 'body') # => #<Net::HTTP::Response>
|
255
|
+
#
|
256
|
+
# connection.post('/example', #<IO Object>) # => #<Net::HTTP::Response>
|
257
|
+
#
|
258
|
+
# connection.post('/example', {:example => 'data'}) # => #<Net::HTTP::Response>
|
259
|
+
#
|
260
|
+
# connection.post('/404') # => raises Changebase::Exception::NotFound
|
261
|
+
#
|
262
|
+
# connection.post('/act') do |response|
|
263
|
+
# # ...
|
264
|
+
# end
|
265
|
+
def post(path, body=nil, &block)
|
266
|
+
request = Net::HTTP::Post.new(path)
|
267
|
+
|
268
|
+
send_request(request, body, &block)
|
269
|
+
end
|
270
|
+
|
271
|
+
# Send a PUT request to +path+ via +Connection#send_request+.
|
272
|
+
# See +Connection#send_request+ for more details on how the response is
|
273
|
+
# handled.
|
274
|
+
#
|
275
|
+
# Paramaters::
|
276
|
+
#
|
277
|
+
# * +path+ - The +path+ on the server to POST to.
|
278
|
+
# * +body+ - Optional, See +Connection#send_request+.
|
279
|
+
# * +block+ - Optional, See +Connection#send_request+
|
280
|
+
#
|
281
|
+
# Return Value::
|
282
|
+
#
|
283
|
+
# See +Connection#send_request+
|
284
|
+
#
|
285
|
+
# Examples:
|
286
|
+
#
|
287
|
+
# #!ruby
|
288
|
+
# connection.put('/example') # => #<Net::HTTP::Response>
|
289
|
+
#
|
290
|
+
# connection.put('/example', 'body') # => #<Net::HTTP::Response>
|
291
|
+
#
|
292
|
+
# connection.put('/example', #<IO Object>) # => #<Net::HTTP::Response>
|
293
|
+
#
|
294
|
+
# connection.put('/example', {:example => 'data'}) # => #<Net::HTTP::Response>
|
295
|
+
#
|
296
|
+
# connection.put('/404') # => raises Changebase::Exception::NotFound
|
297
|
+
#
|
298
|
+
# connection.put('/act') do |response|
|
299
|
+
# # ...
|
300
|
+
# end
|
301
|
+
def put(path, body=nil, *valid_response_codes, &block)
|
302
|
+
request = Net::HTTP::Put.new(path)
|
303
|
+
|
304
|
+
send_request(request, body, &block)
|
305
|
+
end
|
306
|
+
|
307
|
+
# Send a DELETE request to +path+ via +Connection#send_request+.
|
308
|
+
# See +Connection#send_request+ for more details on how the response is
|
309
|
+
# handled
|
310
|
+
#
|
311
|
+
# Paramaters::
|
312
|
+
#
|
313
|
+
# * +path+ - The +path+ on the server to POST to.
|
314
|
+
# * +block+ - Optional, See +Connection#send_request+
|
315
|
+
#
|
316
|
+
# Return Value::
|
317
|
+
#
|
318
|
+
# See +Connection#send_request+
|
319
|
+
#
|
320
|
+
# Examples:
|
321
|
+
#
|
322
|
+
# #!ruby
|
323
|
+
# connection.delete('/example') # => #<Net::HTTP::Response>
|
324
|
+
#
|
325
|
+
# connection.delete('/404') # => raises Changebase::Exception::NotFound
|
326
|
+
#
|
327
|
+
# connection.delete('/act') do |response|
|
328
|
+
# # ...
|
329
|
+
# end
|
330
|
+
def delete(path, &block)
|
331
|
+
request = Net::HTTP::Delete.new(path)
|
332
|
+
|
333
|
+
send_request(request, nil, &block)
|
334
|
+
end
|
335
|
+
|
336
|
+
private
|
337
|
+
|
338
|
+
def request_headers
|
339
|
+
headers = {}
|
340
|
+
|
341
|
+
headers['Accept'] = 'application/json'
|
342
|
+
headers['User-Agent'] = user_agent
|
343
|
+
headers['Api-Version'] = '0.2.0'
|
344
|
+
headers['Connection'] = 'keep-alive'
|
345
|
+
headers['Api-Key'] = api_key if api_key
|
346
|
+
|
347
|
+
headers
|
348
|
+
end
|
349
|
+
|
350
|
+
# Raise an Changebase::Exception based on the response_code, unless the
|
351
|
+
# response_code is include in the valid_response_codes Array
|
352
|
+
#
|
353
|
+
# Paramaters::
|
354
|
+
#
|
355
|
+
# * +response+ - The Net::HTTP::Response object
|
356
|
+
#
|
357
|
+
# Return Value::
|
358
|
+
#
|
359
|
+
# If an exception is not raised the +response+ is returned
|
360
|
+
#
|
361
|
+
# Examples:
|
362
|
+
#
|
363
|
+
# #!ruby
|
364
|
+
# connection.validate_response_code(<Net::HTTP::Response @code=200>) # => <Net::HTTP::Response @code=200>
|
365
|
+
#
|
366
|
+
# connection.validate_response_code(<Net::HTTP::Response @code=404>) # => raises Changebase::Exception::NotFound
|
367
|
+
#
|
368
|
+
# connection.validate_response_code(<Net::HTTP::Response @code=500>) # => raises Changebase::Exception
|
369
|
+
def validate_response_code(response)
|
370
|
+
code = response.code.to_i
|
371
|
+
|
372
|
+
if !(200..299).include?(code)
|
373
|
+
case code
|
374
|
+
when 400
|
375
|
+
raise Changebase::Exception::BadRequest, response.body
|
376
|
+
when 401
|
377
|
+
raise Changebase::Exception::Unauthorized, response.body
|
378
|
+
when 403
|
379
|
+
raise Changebase::Exception::Forbidden, response.body
|
380
|
+
when 404
|
381
|
+
raise Changebase::Exception::NotFound, response.body
|
382
|
+
when 410
|
383
|
+
raise Changebase::Exception::Gone, response.body
|
384
|
+
when 422
|
385
|
+
raise Changebase::Exception::ApiVersionUnsupported, response.body
|
386
|
+
when 503
|
387
|
+
raise Changebase::Exception::ServiceUnavailable, response.body
|
388
|
+
when 301
|
389
|
+
raise Changebase::Exception::MovedPermanently, response.body
|
390
|
+
when 502
|
391
|
+
raise Changebase::Exception::BadGateway, response.body
|
392
|
+
when 500..599
|
393
|
+
raise Changebase::ServerError, response.body
|
394
|
+
else
|
395
|
+
raise Changebase::Exception, response.body
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module Changebase::Inline
|
4
|
+
|
5
|
+
module Through
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
def delete_records(records, method)
|
9
|
+
x = super
|
10
|
+
|
11
|
+
if method != :destroy
|
12
|
+
records.each do |record|
|
13
|
+
through_model = source_reflection.active_record
|
14
|
+
|
15
|
+
columns = through_model.columns.each_with_index.reduce([]) do |acc, (column, index)|
|
16
|
+
attr_type = through_model.type_for_attribute(column.name)
|
17
|
+
previous_value = attr_type.serialize(column.name == source_reflection.foreign_key ? record.id : owner.id)
|
18
|
+
acc << {
|
19
|
+
index: index,
|
20
|
+
identity: true,
|
21
|
+
name: column.name,
|
22
|
+
type: column.sql_type,
|
23
|
+
value: nil,
|
24
|
+
previous_value: previous_value
|
25
|
+
}
|
26
|
+
acc
|
27
|
+
end
|
28
|
+
|
29
|
+
transaction = through_model.connection.changebase_transaction || Changebase::Inline::Transaction.new(
|
30
|
+
timestamp: Time.current,
|
31
|
+
metadata: through_model.connection.instance_variable_get(:@changebase_metadata)
|
32
|
+
)
|
33
|
+
|
34
|
+
transaction.event!({
|
35
|
+
schema: columns[0].try(:[], :schema) || through_model.connection.current_schema,
|
36
|
+
table: through_model.table_name,
|
37
|
+
type: :delete,
|
38
|
+
columns: columns,
|
39
|
+
timestamp: Time.current
|
40
|
+
})
|
41
|
+
|
42
|
+
# Save the Changebase::Transaction if we are not in a transaction.
|
43
|
+
transaction.save! if !through_model.connection.changebase_transaction
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
x
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
module HasMany
|
52
|
+
|
53
|
+
def delete_or_nullify_all_records(method)
|
54
|
+
x = super
|
55
|
+
if method == :delete_all
|
56
|
+
target.each { |record| record.changebase_track(:delete) }
|
57
|
+
end
|
58
|
+
x
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
module ActiveRecord
|
64
|
+
|
65
|
+
module PostgreSQLAdapter
|
66
|
+
|
67
|
+
attr_reader :changebase_transaction
|
68
|
+
|
69
|
+
# Begins a transaction.
|
70
|
+
def begin_db_transaction
|
71
|
+
super
|
72
|
+
@changebase_transaction = Changebase::Inline::Transaction.new(
|
73
|
+
timestamp: Time.current,
|
74
|
+
metadata: @changebase_metadata
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Aborts a transaction.
|
79
|
+
def exec_rollback_db_transaction
|
80
|
+
super
|
81
|
+
ensure
|
82
|
+
@changebase_transaction = nil
|
83
|
+
end
|
84
|
+
|
85
|
+
# Commits a transaction.
|
86
|
+
def commit_db_transaction
|
87
|
+
@changebase_transaction&.save!
|
88
|
+
@changebase_transaction = nil
|
89
|
+
super
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
extend ActiveSupport::Concern
|
94
|
+
|
95
|
+
class_methods do
|
96
|
+
def self.extended(other)
|
97
|
+
other.after_create { changebase_track(:insert) }
|
98
|
+
other.after_update { changebase_track(:update) }
|
99
|
+
other.after_destroy { changebase_track(:delete) }
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
def changebase_tracking
|
105
|
+
if Changebase.configured?# && self.class.instance_variable_defined?(:@changebase)
|
106
|
+
# self.class.instance_variable_get(:@changebase)
|
107
|
+
{exclude: []}
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def changebase_transaction
|
112
|
+
self.class.connection.changebase_transaction
|
113
|
+
end
|
114
|
+
|
115
|
+
def changebase_track(type)
|
116
|
+
return if !changebase_tracking
|
117
|
+
return if type == :update && self.previous_changes.empty?
|
118
|
+
|
119
|
+
# Go through each of the Model#attributes and grab the type from the
|
120
|
+
# Model#type_for_attribute(attr) to do the serialization, grab the
|
121
|
+
# column definition using Model#column_for_attribute(attr) to write the
|
122
|
+
# type, and use Model.columns.index(col) to grab the index of the column
|
123
|
+
# in the database.
|
124
|
+
columns = self.class.columns.each_with_index.reduce([]) do |acc, (column, index)|
|
125
|
+
identity = self.class.primary_key ? self.class.primary_key == column.name : true
|
126
|
+
|
127
|
+
attr_type = self.type_for_attribute(column.name)
|
128
|
+
value = self.attributes[column.name]
|
129
|
+
previous_value = self.previous_changes[column.name].try(:[], 0)
|
130
|
+
|
131
|
+
case type
|
132
|
+
when :update
|
133
|
+
previous_value ||= value
|
134
|
+
when :delete
|
135
|
+
previous_value ||= value
|
136
|
+
value = nil
|
137
|
+
end
|
138
|
+
|
139
|
+
acc << {
|
140
|
+
index: index,
|
141
|
+
identity: identity,
|
142
|
+
name: column.name,
|
143
|
+
type: column.sql_type,
|
144
|
+
value: attr_type.serialize(value),
|
145
|
+
previous_value: attr_type.serialize(previous_value)
|
146
|
+
}
|
147
|
+
acc
|
148
|
+
end
|
149
|
+
|
150
|
+
# Emit the event
|
151
|
+
changebase_transaction.event!({
|
152
|
+
schema: columns[0].try(:[], :schema) || self.class.connection.current_schema,
|
153
|
+
table: self.class.table_name,
|
154
|
+
type: type,
|
155
|
+
columns: columns,
|
156
|
+
timestamp: Time.current
|
157
|
+
})
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Changebase::Inline::Event
|
2
|
+
|
3
|
+
attr_accessor :id, :database_id, :transaction_id, :type, :schema,
|
4
|
+
:table, :timestamp, :created_at, :columns
|
5
|
+
|
6
|
+
def initialize(attrs)
|
7
|
+
attrs.each { |k,v| self.send("#{k}=", v) }
|
8
|
+
|
9
|
+
self.columns ||= {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def as_json
|
13
|
+
{
|
14
|
+
id: id,
|
15
|
+
transaction_id: transaction_id,
|
16
|
+
lsn: timestamp.utc.iso8601(3),
|
17
|
+
type: type,
|
18
|
+
schema: schema,
|
19
|
+
table: table,
|
20
|
+
timestamp: timestamp.utc.iso8601(3),
|
21
|
+
columns: columns.as_json
|
22
|
+
}.compact
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
class Changebase::Inline::Transaction
|
4
|
+
|
5
|
+
attr_accessor :id, :metadata, :timestamp, :events
|
6
|
+
|
7
|
+
def initialize(attrs={})
|
8
|
+
attrs.each { |k,v| self.send("#{k}=", v) }
|
9
|
+
|
10
|
+
if id
|
11
|
+
@persisted = true
|
12
|
+
else
|
13
|
+
@persisted = false
|
14
|
+
@id ||= SecureRandom.uuid
|
15
|
+
end
|
16
|
+
|
17
|
+
@events ||= []
|
18
|
+
@timestamp ||= Time.now
|
19
|
+
@metadata ||= {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def persisted?
|
23
|
+
@persisted
|
24
|
+
end
|
25
|
+
|
26
|
+
def event!(event_attributes)
|
27
|
+
event = Changebase::Inline::Event.new(event_attributes)
|
28
|
+
@events << event
|
29
|
+
event
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.create!(attrs={})
|
33
|
+
transaction = self.new(attrs)
|
34
|
+
transaction.save!
|
35
|
+
transaction
|
36
|
+
end
|
37
|
+
|
38
|
+
def save!
|
39
|
+
persisted? ? _update : _create
|
40
|
+
end
|
41
|
+
|
42
|
+
def _update
|
43
|
+
return if events.empty?
|
44
|
+
events.delete_if { |a| a.diff.empty? }
|
45
|
+
payload = JSON.generate({events: events.as_json.map{ |json| json[:transaction_id] = id; json }})
|
46
|
+
Changebase.logger.debug("[Changebase] POST /events WITH #{payload}")
|
47
|
+
Changebase.connection.post('/events', payload)
|
48
|
+
@events = []
|
49
|
+
end
|
50
|
+
|
51
|
+
def _create
|
52
|
+
events.delete_if { |a| a.columns.empty? }
|
53
|
+
payload = JSON.generate({transaction: self.as_json})
|
54
|
+
Changebase.logger.debug("[Changebase] POST /transactions WITH #{payload}")
|
55
|
+
Changebase.connection.post('/transactions', payload)
|
56
|
+
@events = []
|
57
|
+
@persisted = true
|
58
|
+
end
|
59
|
+
|
60
|
+
def as_json
|
61
|
+
result = {
|
62
|
+
id: id,
|
63
|
+
lsn: timestamp.utc.iso8601(3),
|
64
|
+
timestamp: timestamp.utc.iso8601(3),
|
65
|
+
events: events.as_json
|
66
|
+
}
|
67
|
+
result[:metadata] = metadata.as_json if !metadata.empty?
|
68
|
+
result
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'changebase'
|
2
|
+
Changebase.mode = 'inline'
|
3
|
+
|
4
|
+
module Changebase::Inline
|
5
|
+
|
6
|
+
autoload :Event, 'changebase/inline/event'
|
7
|
+
autoload :Transaction, 'changebase/inline/transaction'
|
8
|
+
|
9
|
+
def self.load!
|
10
|
+
require 'active_record'
|
11
|
+
require 'changebase/active_record'
|
12
|
+
|
13
|
+
::ActiveRecord::Base.include(Changebase::ActiveRecord)
|
14
|
+
::ActiveRecord::ConnectionAdapters::AbstractAdapter.include(Changebase::ActiveRecord::Connection)
|
15
|
+
|
16
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
17
|
+
require 'changebase/inline/active_record'
|
18
|
+
::ActiveRecord::Base.include(Changebase::Inline::ActiveRecord)
|
19
|
+
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.include(Changebase::Inline::ActiveRecord::PostgreSQLAdapter)
|
20
|
+
::ActiveRecord::Associations::HasManyThroughAssociation.prepend(Changebase::Inline::Through)
|
21
|
+
::ActiveRecord::Associations::HasManyAssociation.prepend(Changebase::Inline::HasMany)
|
22
|
+
|
23
|
+
@loaded = true
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.loaded?
|
27
|
+
@loaded
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
Changebase::Inline.load!
|
data/lib/changebase/railtie.rb
CHANGED
@@ -1,23 +1,31 @@
|
|
1
1
|
class Changebase::Engine < ::Rails::Engine
|
2
2
|
|
3
3
|
config.changebase = ActiveSupport::OrderedOptions.new
|
4
|
+
# config.changebase.mode = nil
|
4
5
|
config.changebase.metadata_table = "changebase_metadata"
|
5
|
-
|
6
|
+
|
6
7
|
initializer :changebase do |app|
|
7
8
|
migration_paths = config.paths['db/migrate'].expanded
|
8
|
-
|
9
|
+
|
9
10
|
ActiveSupport.on_load(:active_record) do
|
10
|
-
|
11
|
-
|
12
|
-
|
11
|
+
Changebase.logger = ActiveRecord::Base.logger
|
12
|
+
|
13
|
+
case Changebase.mode
|
14
|
+
when 'replication'
|
15
|
+
Changebase::Replication.load! if !Changebase::Replication.loaded?
|
16
|
+
migration_paths.each do |path|
|
17
|
+
ActiveRecord::Tasks::DatabaseTasks.migrations_paths << path
|
18
|
+
end
|
19
|
+
when 'inline'
|
20
|
+
Changebase::Inline.load! if !Changebase::Inline.loaded?
|
13
21
|
end
|
14
22
|
end
|
15
|
-
|
23
|
+
|
16
24
|
ActiveSupport.on_load(:action_controller) do
|
17
25
|
require 'changebase/action_controller'
|
18
26
|
end
|
19
|
-
|
20
|
-
Changebase.
|
27
|
+
|
28
|
+
Changebase.configure(**app.config.changebase.to_h)
|
21
29
|
end
|
22
|
-
|
23
|
-
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Changebase::Replication
|
2
|
+
module ActiveRecord
|
3
|
+
module PostgreSQLAdapter
|
4
|
+
|
5
|
+
def initialize(*args, **margs)
|
6
|
+
@without_changebase = false
|
7
|
+
@changebase_metadata = nil
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def without_changebase
|
12
|
+
@without_changebase = true
|
13
|
+
yield
|
14
|
+
ensure
|
15
|
+
@without_changebase = false
|
16
|
+
end
|
17
|
+
|
18
|
+
def drop_database(name) # :nodoc:
|
19
|
+
without_changebase { super }
|
20
|
+
end
|
21
|
+
|
22
|
+
def drop_table(table_name, **options) # :nodoc:
|
23
|
+
without_changebase { super }
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_database(name, options = {})
|
27
|
+
without_changebase { super }
|
28
|
+
end
|
29
|
+
|
30
|
+
def recreate_database(name, options = {}) # :nodoc:
|
31
|
+
without_changebase { super }
|
32
|
+
end
|
33
|
+
|
34
|
+
def execute(sql, name = nil)
|
35
|
+
if !@without_changebase && !current_transaction.open? && write_query?(sql)
|
36
|
+
transaction { super }
|
37
|
+
else
|
38
|
+
super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def exec_query(sql, name = "SQL", binds = [], prepare: false)
|
43
|
+
if !@without_changebase && !current_transaction.open? && write_query?(sql)
|
44
|
+
transaction { super }
|
45
|
+
else
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def exec_delete(sql, name = nil, binds = []) # :nodoc:
|
51
|
+
if !@without_changebase && !current_transaction.open? && write_query?(sql)
|
52
|
+
transaction { super }
|
53
|
+
else
|
54
|
+
super
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def commit_db_transaction
|
59
|
+
if !@without_changebase && @changebase_metadata && !@changebase_metadata.empty?
|
60
|
+
sql = ::ActiveRecord::Base.send(:replace_named_bind_variables, <<~SQL, {version: 1, metadata: ActiveSupport::JSON.encode(@changebase_metadata)})
|
61
|
+
INSERT INTO #{quote_table_name(Changebase.metadata_table)} ( version, data )
|
62
|
+
VALUES ( :version, :metadata )
|
63
|
+
ON CONFLICT ( version )
|
64
|
+
DO UPDATE SET version = :version, data = :metadata;
|
65
|
+
SQL
|
66
|
+
|
67
|
+
log(sql, "CHANGEBASE") do
|
68
|
+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
|
69
|
+
@connection.async_exec(sql)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
super
|
74
|
+
end
|
75
|
+
|
76
|
+
if ::ActiveRecord.gem_version < ::Gem::Version.new("6.0.0")
|
77
|
+
CHANGEBASE_COMMENT_REGEX = %r{(?:--.*\n)|/\*(?:[^*]|\*[^/])*\*/}m
|
78
|
+
def self.changebase_build_read_query_regexp(*parts) # :nodoc:
|
79
|
+
parts += [:begin, :commit, :explain, :release, :rollback, :savepoint, :select, :with]
|
80
|
+
parts = parts.map { |part| /#{part}/i }
|
81
|
+
/\A(?:[(\s]|#{CHANGEBASE_COMMENT_REGEX})*#{Regexp.union(*parts)}/
|
82
|
+
end
|
83
|
+
|
84
|
+
CHANGEBASE_READ_QUERY = changebase_build_read_query_regexp(
|
85
|
+
:close, :declare, :fetch, :move, :set, :show
|
86
|
+
)
|
87
|
+
def write_query?(sql)
|
88
|
+
!CHANGEBASE_READ_QUERY.match?(sql)
|
89
|
+
rescue ArgumentError # Invalid encoding
|
90
|
+
!CHANGEBASE_READ_QUERY.match?(sql.b)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'changebase'
|
2
|
+
Changebase.mode = 'replication'
|
3
|
+
|
4
|
+
module Changebase::Replication
|
5
|
+
def self.load!
|
6
|
+
require 'active_record'
|
7
|
+
|
8
|
+
::ActiveRecord::Base.include(Changebase::ActiveRecord)
|
9
|
+
::ActiveRecord::ConnectionAdapters::AbstractAdapter.include(Changebase::ActiveRecord::Connection)
|
10
|
+
|
11
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
12
|
+
require 'changebase/replication/active_record'
|
13
|
+
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.include(Changebase::Replication::ActiveRecord::PostgreSQLAdapter)
|
14
|
+
|
15
|
+
@loaded = true
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.loaded?
|
19
|
+
@loaded
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
Changebase::Replication.load!
|
data/lib/changebase/version.rb
CHANGED
data/lib/changebase.rb
CHANGED
@@ -1,16 +1,67 @@
|
|
1
1
|
module Changebase
|
2
|
+
|
3
|
+
autoload :VERSION, 'changebase/version'
|
4
|
+
autoload :Connection, 'changebase/connection'
|
5
|
+
autoload :Inline, 'changebase/inline'
|
6
|
+
autoload :Replication, 'changebase/replication'
|
2
7
|
autoload :ActiveRecord, 'changebase/active_record'
|
3
8
|
autoload :ActionController, 'changebase/action_controller'
|
4
|
-
|
5
|
-
@
|
6
|
-
|
9
|
+
|
10
|
+
@config = {
|
11
|
+
mode: "replication",
|
12
|
+
metadata_table: "changebase_metadata"
|
13
|
+
}
|
14
|
+
|
7
15
|
def self.metadata_table=(value)
|
8
|
-
@metadata_table = value
|
16
|
+
@config[:metadata_table] = value
|
9
17
|
end
|
10
|
-
|
18
|
+
|
11
19
|
def self.metadata_table
|
12
|
-
@metadata_table
|
20
|
+
@config[:metadata_table]
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.mode=(value)
|
24
|
+
@config[:mode] = value
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.mode
|
28
|
+
@config[:mode]
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.connection=(value)
|
32
|
+
@config[:connection] = value
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.connection
|
36
|
+
Thread.current[:changebase_connection] ||= Changebase::Connection.new({
|
37
|
+
url: @config[:connection]
|
38
|
+
})
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.configure(**config)
|
42
|
+
@config.merge!(config)
|
43
|
+
self.logger = @config[:logger] if @config[:logger]
|
13
44
|
end
|
45
|
+
|
46
|
+
def self.configured?
|
47
|
+
case @config[:mode]
|
48
|
+
when 'inline'
|
49
|
+
!!@config[:connection]
|
50
|
+
else
|
51
|
+
true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.logger
|
56
|
+
return @logger if defined?(@logger)
|
57
|
+
|
58
|
+
@logger = Logger.new(STDOUT)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.logger=(logger)
|
62
|
+
@logger = logger
|
63
|
+
end
|
64
|
+
|
14
65
|
end
|
15
66
|
|
16
|
-
require 'changebase/railtie' if defined?(Rails)
|
67
|
+
require 'changebase/railtie' if defined?(Rails)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: changebase
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.2'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jon Bracy
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2022-
|
12
|
+
date: 2022-06-08 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -17,7 +17,7 @@ dependencies:
|
|
17
17
|
requirements:
|
18
18
|
- - ">="
|
19
19
|
- !ruby/object:Gem::Version
|
20
|
-
version: '
|
20
|
+
version: '5.2'
|
21
21
|
- - "<"
|
22
22
|
- !ruby/object:Gem::Version
|
23
23
|
version: '8'
|
@@ -27,7 +27,7 @@ dependencies:
|
|
27
27
|
requirements:
|
28
28
|
- - ">="
|
29
29
|
- !ruby/object:Gem::Version
|
30
|
-
version: '
|
30
|
+
version: '5.2'
|
31
31
|
- - "<"
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '8'
|
@@ -37,7 +37,7 @@ dependencies:
|
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '5.2'
|
41
41
|
- - "<"
|
42
42
|
- !ruby/object:Gem::Version
|
43
43
|
version: '8'
|
@@ -47,7 +47,7 @@ dependencies:
|
|
47
47
|
requirements:
|
48
48
|
- - ">="
|
49
49
|
- !ruby/object:Gem::Version
|
50
|
-
version: '
|
50
|
+
version: '5.2'
|
51
51
|
- - "<"
|
52
52
|
- !ruby/object:Gem::Version
|
53
53
|
version: '8'
|
@@ -57,7 +57,7 @@ dependencies:
|
|
57
57
|
requirements:
|
58
58
|
- - ">="
|
59
59
|
- !ruby/object:Gem::Version
|
60
|
-
version: '
|
60
|
+
version: '5.2'
|
61
61
|
- - "<"
|
62
62
|
- !ruby/object:Gem::Version
|
63
63
|
version: '8'
|
@@ -67,7 +67,7 @@ dependencies:
|
|
67
67
|
requirements:
|
68
68
|
- - ">="
|
69
69
|
- !ruby/object:Gem::Version
|
70
|
-
version: '
|
70
|
+
version: '5.2'
|
71
71
|
- - "<"
|
72
72
|
- !ruby/object:Gem::Version
|
73
73
|
version: '8'
|
@@ -141,6 +141,20 @@ dependencies:
|
|
141
141
|
- - ">="
|
142
142
|
- !ruby/object:Gem::Version
|
143
143
|
version: '0'
|
144
|
+
- !ruby/object:Gem::Dependency
|
145
|
+
name: webmock
|
146
|
+
requirement: !ruby/object:Gem::Requirement
|
147
|
+
requirements:
|
148
|
+
- - ">="
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: '0'
|
151
|
+
type: :development
|
152
|
+
prerelease: false
|
153
|
+
version_requirements: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - ">="
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
144
158
|
- !ruby/object:Gem::Dependency
|
145
159
|
name: mocha
|
146
160
|
requirement: !ruby/object:Gem::Requirement
|
@@ -225,7 +239,14 @@ files:
|
|
225
239
|
- lib/changebase.rb
|
226
240
|
- lib/changebase/action_controller.rb
|
227
241
|
- lib/changebase/active_record.rb
|
242
|
+
- lib/changebase/connection.rb
|
243
|
+
- lib/changebase/inline.rb
|
244
|
+
- lib/changebase/inline/active_record.rb
|
245
|
+
- lib/changebase/inline/event.rb
|
246
|
+
- lib/changebase/inline/transaction.rb
|
228
247
|
- lib/changebase/railtie.rb
|
248
|
+
- lib/changebase/replication.rb
|
249
|
+
- lib/changebase/replication/active_record.rb
|
229
250
|
- lib/changebase/version.rb
|
230
251
|
homepage: https://changebase.io
|
231
252
|
licenses: []
|