db_wrapper 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bd375ea049a96b97ec0275f07022f986d8bda96b
4
+ data.tar.gz: 4e8f284605c832eec9426b2b5f8d2675b089e3f9
5
+ SHA512:
6
+ metadata.gz: e1a2f2347b482b5770952d859769c73075f744af21879565ffaac7d5a8ccd1b55c9cf0807a742a7b90461f69528398d44e539e1efc9b139b23ee259ecfd5adf2
7
+ data.tar.gz: 51a543f85b9ffdce8112e284b5b368c9bb71c4f6f7e0850f73709e6d0a07fee472be38be69b0e227cdce2cbde820a1f02eb2bf6c1a8dd0154c617bd2ae9c9423
@@ -0,0 +1,7 @@
1
+ .rvmrc
2
+ .ruby-version
3
+ Gemfile.lock
4
+ .bundle/
5
+ .rspec
6
+ .idea
7
+ db_wrapper*.gem
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
@@ -0,0 +1,33 @@
1
+ # db_wrapper #
2
+
3
+ ## What is db_wrapper ? ##
4
+
5
+ db_wrapper is a Ruby gem that allows the creation of listeners to every database call you make in a transparent way.
6
+
7
+ ## How does it work ? ##
8
+
9
+ It creates a super lightweight TCP proxy that will redirect every database call to the proxied database (in a **non blocking way**) to another process that will call the listeners you registered. Everything happens in a different process so the listeners won't impact the database query performance.
10
+
11
+ The fact that it works like a proxy allows you to create listeners without changing your application code and without needing to worry about performance.
12
+
13
+ ## Which databases does it support ? ##
14
+
15
+ * Mysql
16
+ * PostgreSQL (future)
17
+ * MongoDB (future)
18
+
19
+ The protocol implementation is simple so you can easily extend it by yourself to support a
20
+ different database (and send me the pull request if you want)
21
+
22
+ ## Next steps ##
23
+ * PostgreSQL protocol
24
+ * MongoDB protocol
25
+ * Examples and documentation
26
+ * Server listeners - Listeners that would get data sent from the server to the client, also in a non blocking way
27
+
28
+ ## License and copyright ##
29
+
30
+ db_wrapper is copyrighted free software made available under the terms
31
+ of either the GPL or Ruby's License.
32
+
33
+ Copyright: (C) 2014 by Pedro Sena. All Rights Reserved.
@@ -0,0 +1,6 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new('spec')
4
+
5
+ # If you want to make this the default task
6
+ task :default => :spec
@@ -0,0 +1,30 @@
1
+ require 'benchmark'
2
+ require 'mysql2'
3
+ require_relative '../lib/db_wrapper'
4
+
5
+ client = Mysql2::Client.new host: '127.0.0.1', username: 'db_wrapper', password: 'db_wrapper', database: 'db_wrapper_benchmark'
6
+
7
+ insert = "insert into test values(null, 'some description here', now(), null)"
8
+ select = 'select * from test'
9
+ delete = 'delete from test'
10
+ update = "update test set description = 'another description'"
11
+
12
+ iterations = ARGV.first.to_i
13
+
14
+ Benchmark.bm do |bm|
15
+
16
+ bm.report('Proxied access') do
17
+ client = Mysql2::Client.new host: '127.0.0.1', username: 'db_wrapper', password: 'db_wrapper', database: 'db_wrapper_benchmark', port: 3307
18
+ iterations.times do
19
+ client.query select
20
+ end
21
+ end
22
+
23
+ bm.report('Direct access') do
24
+ client = Mysql2::Client.new host: '127.0.0.1', username: 'db_wrapper', password: 'db_wrapper', database: 'db_wrapper_benchmark', port: 3306
25
+ iterations.times do
26
+ client.query select
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,9 @@
1
+ require_relative '../lib/db_wrapper'
2
+
3
+ database_proxy = DBWrapper::DatabaseProxy.new '127.0.0.1', 3307, '127.0.0.1', 3306
4
+ database_proxy.protocol = DBWrapper::MysqlProtocol.new
5
+ database_proxy.add_client_listener(DBWrapper::Listeners::Select.new do
6
+ puts 'SelectListener found command: ' + command
7
+ end)
8
+
9
+ database_proxy.start!
@@ -0,0 +1,21 @@
1
+ require File.expand_path('../lib/db_wrapper/version', __FILE__)
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'db_wrapper'
5
+ spec.version = DBWrapper::VERSION
6
+ spec.date = '2014-01-24'
7
+ spec.description = spec.summary = 'Create custom ruby listeners/interceptors for any database'
8
+ spec.authors = ['Pedro Sena']
9
+ spec.email = 'sena.pedro@gmail.com'
10
+ spec.homepage = 'https://github.com/PedroSena/db_wrapper'
11
+ spec.license = 'MIT'
12
+
13
+ spec.add_development_dependency 'rspec'
14
+ spec.add_development_dependency 'em-http-request'
15
+ spec.add_dependency 'em-proxy'
16
+ spec.add_dependency 'log4r'
17
+
18
+ spec.files = `git ls-files`.split "\n"
19
+ spec.test_files = `git ls-files -- spec/*`.split "\n"
20
+ spec.require_paths = %w(lib)
21
+ end
@@ -0,0 +1,11 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/db_wrapper')
2
+
3
+ require 'em-proxy'
4
+ require 'log4r'
5
+ require 'socket'
6
+
7
+ Log = Log4r::Logger.new 'db_wrapper'
8
+ Log.add Log4r::Outputter.stderr
9
+ Log.level = Log4r::DEBUG
10
+
11
+ Gem.find_files("#{File.dirname(__FILE__)}/**/*.rb").each { |file| require file }
@@ -0,0 +1,54 @@
1
+ module DBWrapper
2
+ class DatabaseProxy
3
+
4
+ attr_reader :host, :port, :database_host, :database_port, :listener_server_socket
5
+ attr_accessor :protocol
6
+
7
+ def initialize(host, port, database_host, database_port)
8
+ @host = host
9
+ @port = port
10
+ @database_host = database_host
11
+ @database_port = database_port
12
+ @client_listeners = []
13
+ end
14
+
15
+ def add_client_listener(client_listener)
16
+ @client_listeners << client_listener
17
+ end
18
+
19
+ def start!
20
+ raise 'No protocol was given' if self.protocol.nil?
21
+ listener_server_port = create_listener_server.addr[1]
22
+ @listener_server_socket = TCPSocket.new(self.host, listener_server_port)
23
+ database_proxy = self
24
+ Proxy.start(host: @host, port: @port) do |conn|
25
+ conn.server :database, host: database_proxy.database_host, port: database_proxy.database_port, relay_server: true
26
+
27
+ conn.on_data do |data|
28
+ database_proxy.listener_server_socket.write_nonblock(data)
29
+ data
30
+ end
31
+
32
+ conn.on_finish do |server, name|
33
+ unbind if server == :database
34
+ end
35
+ end
36
+ end
37
+
38
+ def create_listener_server
39
+ listener_server = TCPServer.new(host, 0) #Creates on first free port it can find
40
+ client_listeners_controller = ListenersController.new @client_listeners
41
+ fork do
42
+ Socket.accept_loop(listener_server) do |connection|
43
+ begin
44
+ while data = connection.readpartial(self.protocol.max_packet_size) do
45
+ client_listeners_controller.call_listeners(self.protocol, data)
46
+ end
47
+ rescue Interrupt; end #Thrown when you send a termination signal
48
+ end
49
+ end
50
+ listener_server
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,22 @@
1
+ #Base listener for all data sent from the client to the database
2
+ module DBWrapper
3
+ module Listeners
4
+ class ClientListener
5
+
6
+ attr_accessor :command
7
+
8
+ def initialize(&block)
9
+ @block = block
10
+ end
11
+
12
+ def perform
13
+ instance_eval(&@block)
14
+ end
15
+
16
+ def listening?(query)
17
+ true
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ require 'listeners/client_listener'
2
+ require 'listeners/simple_command_detector'
3
+
4
+ module DBWrapper
5
+ module Listeners
6
+ class Select < DBWrapper::Listeners::ClientListener; include DBWrapper::SimpleCommandDetector; end
7
+ class Insert < DBWrapper::Listeners::ClientListener; include DBWrapper::SimpleCommandDetector; end
8
+ class Update < DBWrapper::Listeners::ClientListener; include DBWrapper::SimpleCommandDetector; end
9
+ class Delete < DBWrapper::Listeners::ClientListener; include DBWrapper::SimpleCommandDetector; end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module DBWrapper
2
+ module SimpleCommandDetector
3
+ def listening?(command)
4
+ self.class.name.split('::').last.downcase == command.split(' ').first.downcase
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ module DBWrapper
2
+ class ListenersController
3
+
4
+ def initialize(listeners)
5
+ @listeners = listeners
6
+ end
7
+
8
+ def call_listeners(protocol, raw_command)
9
+ return if @listeners.nil?
10
+ parsed_command = protocol.parse_command raw_command
11
+ return if parsed_command.empty?
12
+ @listeners.select { |listener| listener.listening?(parsed_command) }.each do |listener|
13
+ listener.command = parsed_command
14
+ listener.perform
15
+ end
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ module DBWrapper
2
+ class MysqlProtocol
3
+ def parse_command(dirty_command)
4
+ dirty_command.byteslice(5, dirty_command.length)
5
+ end
6
+ def max_packet_size
7
+ 2 ** 24 - 1
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ class String
2
+ def underscore
3
+ word = self.dup
4
+ word.gsub!(/::/, '/')
5
+ word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
6
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
7
+ word.tr!("-", "_")
8
+ word.downcase!
9
+ word
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module DBWrapper
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+ require 'em-http-request'
3
+
4
+ describe DBWrapper::DatabaseProxy do
5
+
6
+ let(:proxy) { DBWrapper::DatabaseProxy.new '127.0.0.1','3307', '127.0.0.1', '3306' }
7
+
8
+ it 'should allow only read of proxy and db connection data' do
9
+ %w(host port database_host database_port).each do |attr|
10
+ expect(proxy.send(attr.to_sym)).to_not be_nil
11
+ expect(proxy.respond_to?("#{attr}=".to_sym)).to be false
12
+ end
13
+ end
14
+
15
+ it 'calls the client listeners while running' do
16
+ called = false
17
+ EM.run do
18
+ EventMachine.add_timer(0.1) do
19
+ sock = TCPSocket.new '127.0.0.1', 3307
20
+ sock.send "\x10\x00\x00\x00\x03" + 'Select 1 from something;', 0
21
+ sock.close
22
+ end
23
+ EventMachine.add_timer(0.2) do
24
+ EM.stop
25
+ end
26
+
27
+ proxy.protocol = DBWrapper::MysqlProtocol.new
28
+ listener = DBWrapper::Listeners::Select.new do
29
+ called = true
30
+ end
31
+ proxy.add_client_listener(listener)
32
+ Socket.should_receive(:accept_loop) do |&block|
33
+ listener.perform
34
+ end
35
+ proxy.should_receive(:fork) do |&block|
36
+ block.call
37
+ end
38
+ proxy.start!
39
+ end
40
+ expect(called).to be true
41
+ end
42
+
43
+ describe 'add_client_listener' do
44
+ let(:listener) { DBWrapper::Listeners::Select.new do; end }
45
+ before(:each) do
46
+ proxy.add_client_listener listener
47
+ end
48
+ let(:client_listeners) { proxy.instance_variable_get(:@client_listeners) }
49
+
50
+ it 'adds a listener' do
51
+ expect(client_listeners.size).to eq 1
52
+ end
53
+ end
54
+
55
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe DBWrapper::Listeners::ClientListener do
4
+ let(:client_listener) { DBWrapper::Listeners::ClientListener.new do; end }
5
+ it 'is listening to every query' do
6
+ ['select 1 from a', 'insert into a(col1) values(1)', 'update a set col1=val1 where col1=something', 'delete from a where col1=something'].each do |query|
7
+ expect(client_listener.listening?(query)).to be true
8
+ end
9
+ end
10
+
11
+ it 'executes the code block on perform' do
12
+ executed = false
13
+ listener = DBWrapper::Listeners::ClientListener.new do
14
+ executed = true
15
+ end
16
+ listener.perform
17
+ expect(executed).to be true
18
+ end
19
+
20
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ describe DBWrapper::SimpleCommandDetector do
4
+
5
+ let(:commands) { ['select 1 from a', 'insert into a(col1) values(val1)', 'update a set col1=val1', 'delete from a'] }
6
+
7
+ %w(select insert update delete).each do |command|
8
+ describe "#{command}" do
9
+ it "listens only to #{command} commands" do
10
+ correct_command = commands.select { |sql_command| sql_command.start_with?(command) }.first
11
+ listener = get_listener(correct_command)
12
+ expect(listener.listening?(correct_command)).to be true
13
+ commands.reject { |sql_command| sql_command == correct_command }.each do |unallowed_command|
14
+ expect(listener.listening?(unallowed_command)).to be false
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ #Object.const_get didnt work in rubinius =/
21
+ def get_listener(command)
22
+ case command.split(' ').first
23
+ when 'select' then DBWrapper::Listeners::Select.new
24
+ when 'insert' then DBWrapper::Listeners::Insert.new
25
+ when 'update' then DBWrapper::Listeners::Update.new
26
+ when 'delete' then DBWrapper::Listeners::Delete.new
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe DBWrapper::ListenersController do
4
+
5
+ describe 'call_listeners' do
6
+ let(:listener) { DBWrapper::Listeners::Select.new { raise self.command } }
7
+
8
+ it 'calls #perform on the listener' do
9
+ module EventMachine
10
+ def self.defer(op = nil, callback = nil, &blk)
11
+ op.call
12
+ end
13
+ end
14
+ EM.run do
15
+ controller = DBWrapper::ListenersController.new [listener]
16
+ command = 'select 1 from a'
17
+ protocol = Object.new
18
+ allow(protocol).to receive(:parse_command).and_return(command)
19
+ allow(protocol).to receive(:detect_interested_observers).and_return([:select])
20
+ expect { controller.call_listeners protocol, command }.to raise_error(command)
21
+ EM.stop
22
+ end
23
+ end
24
+
25
+ it 'will ignore if no listener is bound to specific command' do
26
+ controller = DBWrapper::ListenersController.new([])
27
+ command = 'select 1 from a'
28
+ protocol = Object.new
29
+ allow(protocol).to receive(:parse_command).and_return(command)
30
+ allow(protocol).to receive(:detect_interested_observers).and_return([:select])
31
+ controller.call_listeners protocol, command
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe DBWrapper::MysqlProtocol do
4
+ let(:protocol) { DBWrapper::MysqlProtocol.new }
5
+
6
+ describe 'parse_command - removing non-sql data' do
7
+ def test_parse(clean_string, dirty_string)
8
+ expect(protocol.parse_command(dirty_string)).to eq clean_string
9
+ end
10
+ it 'parses a select command' do
11
+ clean_string = 'select a from b'
12
+ dirty_string = "\x10\x00\x00\x00\x03" + clean_string
13
+ test_parse clean_string, dirty_string
14
+ end
15
+
16
+ it 'parses a insert command' do
17
+ clean_string = 'insert into a(col1) values(1)'
18
+ dirty_string = "\x1E\x00\x00\x00\x03" + clean_string
19
+ test_parse clean_string, dirty_string
20
+ end
21
+
22
+ it 'parses a update command' do
23
+ clean_string = "update a set col1 = 'val1' where col1 > 1"
24
+ dirty_string = "*\x00\x00\x00\x03" + clean_string
25
+ test_parse clean_string, dirty_string
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'db_wrapper'
4
+
5
+ RSpec.configure do |config|
6
+
7
+ end
8
+
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: db_wrapper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Pedro Sena
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: em-http-request
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: em-proxy
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: log4r
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Create custom ruby listeners/interceptors for any database
70
+ email: sena.pedro@gmail.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - .gitignore
76
+ - Gemfile
77
+ - README.md
78
+ - Rakefile
79
+ - benchmark/mysql_access.rb
80
+ - benchmark/mysql_proxy.rb
81
+ - db_wrapper.gemspec
82
+ - lib/db_wrapper.rb
83
+ - lib/db_wrapper/database_proxy.rb
84
+ - lib/db_wrapper/listeners/client_listener.rb
85
+ - lib/db_wrapper/listeners/crud.rb
86
+ - lib/db_wrapper/listeners/simple_command_detector.rb
87
+ - lib/db_wrapper/listeners_controller.rb
88
+ - lib/db_wrapper/protocols/mysql_protocol.rb
89
+ - lib/db_wrapper/util/underscore.rb
90
+ - lib/db_wrapper/version.rb
91
+ - spec/database_proxy_spec.rb
92
+ - spec/listeners/client_listener_spec.rb
93
+ - spec/listeners/crud_spec.rb
94
+ - spec/listeners_controller_spec.rb
95
+ - spec/protocols/mysql_protocol_spec.rb
96
+ - spec/spec_helper.rb
97
+ homepage: https://github.com/PedroSena/db_wrapper
98
+ licenses:
99
+ - MIT
100
+ metadata: {}
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - '>='
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubyforge_project:
117
+ rubygems_version: 2.1.11
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: Create custom ruby listeners/interceptors for any database
121
+ test_files:
122
+ - spec/database_proxy_spec.rb
123
+ - spec/listeners/client_listener_spec.rb
124
+ - spec/listeners/crud_spec.rb
125
+ - spec/listeners_controller_spec.rb
126
+ - spec/protocols/mysql_protocol_spec.rb
127
+ - spec/spec_helper.rb