db_wrapper 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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