changebase 0.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|