em-pg-client-helper 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,45 @@
1
+ While [`em-pg-client`](https://github.com/royaltm/ruby-em-pg-client) is a
2
+ nice solution to the problem of accessing a PostgreSQL database from within
3
+ [EventMachine](http://rubyeventmachine.com/), it is somewhat... spartan.
4
+ ORMs have spoiled us somewhat, and so the appeal of hand-writing SQL and
5
+ dealing with the finer details of error handling has faded. Hence the
6
+ creation of the `EM::PG::Client::Helper` module. It contains a collection
7
+ of useful helper methods that make it somewhat easier to perform common
8
+ operations against PgSQL databases.
9
+
10
+
11
+ # Installation
12
+
13
+ It's a gem:
14
+
15
+ gem install em-pg-client-helper
16
+
17
+ If you're the sturdy type that likes to run from git:
18
+
19
+ rake build; gem install pkg/em-pg-client-helper-<whatever>.gem
20
+
21
+ Or, if you've eschewed the convenience of Rubygems, then you presumably know
22
+ what to do already.
23
+
24
+
25
+ # Usage
26
+
27
+ To use any of these methods, you will want to add the following require:
28
+
29
+ require 'em-pg-client-helper'
30
+
31
+ Then add this line in any classes you wish to use the helper methods in:
32
+
33
+ include PG::EM::Client::Helper
34
+
35
+ The module documentation for EM::PG::Client::Helper has more information on
36
+ the available methods and how to use them.
37
+
38
+
39
+ # Contributing
40
+
41
+ Bug reports should be sent to the [Github issue
42
+ tracker](https://github.com/mpalmer/em-pg-client-helper/issues), or
43
+ [e-mailed](mailto:theshed+em-pg-client-helper@hezmatt.org). Patches can be
44
+ sent as a Github pull request, or
45
+ [e-mailed](mailto:theshed+em-pg-client-helper@hezmatt.org).
@@ -0,0 +1,41 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ task :default => :test
5
+
6
+ begin
7
+ Bundler.setup(:default, :development)
8
+ rescue Bundler::BundlerError => e
9
+ $stderr.puts e.message
10
+ $stderr.puts "Run `bundle install` to install missing gems"
11
+ exit e.status_code
12
+ end
13
+
14
+ Bundler::GemHelper.install_tasks
15
+
16
+ task :release do
17
+ sh "git release"
18
+ end
19
+
20
+ require 'rdoc/task'
21
+
22
+ RDoc::Task.new do |rd|
23
+ rd.main = "README.md"
24
+ rd.title = 'em-pg-client-helper'
25
+ rd.markup = "markdown"
26
+ rd.rdoc_files.include("README.md", "lib/**/*.rb")
27
+ end
28
+
29
+ desc "Run guard"
30
+ task :guard do
31
+ require 'guard'
32
+ ::Guard.start(:clear => true)
33
+ while ::Guard.running do
34
+ sleep 0.5
35
+ end
36
+ end
37
+
38
+ require 'rspec/core/rake_task'
39
+ RSpec::Core::RakeTask.new :test do |t|
40
+ t.pattern = "spec/**/*_spec.rb"
41
+ end
@@ -0,0 +1,32 @@
1
+ require 'git-version-bump'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "em-pg-client-helper"
5
+
6
+ s.version = GVB.version
7
+ s.date = GVB.date
8
+
9
+ s.platform = Gem::Platform::RUBY
10
+
11
+ s.homepage = "http://github.com/mpalmer/em-pg-client-helper"
12
+ s.summary = "Simplify common operations using em-pg-client"
13
+ s.authors = ["Matt Palmer"]
14
+
15
+ s.extra_rdoc_files = ["README.md"]
16
+ s.files = `git ls-files`.split("\n")
17
+
18
+ s.add_runtime_dependency "git-version-bump", "~> 0.10"
19
+ s.add_runtime_dependency "em-pg-client", "~> 0.3"
20
+
21
+ s.add_development_dependency 'bundler'
22
+ s.add_development_dependency 'eventmachine'
23
+ s.add_development_dependency 'github-release'
24
+ s.add_development_dependency 'guard-spork'
25
+ s.add_development_dependency 'guard-rspec'
26
+ # Needed for guard
27
+ s.add_development_dependency 'rb-inotify', '~> 0.9'
28
+ s.add_development_dependency 'pry-debugger'
29
+ s.add_development_dependency 'rake'
30
+ s.add_development_dependency 'rdoc'
31
+ s.add_development_dependency 'rspec'
32
+ end
@@ -0,0 +1,148 @@
1
+ require 'pg/em'
2
+ require 'pg/em/connection_pool'
3
+
4
+ # Some helper methods to make working with em-pg-client slightly less like
5
+ # trying to build a house with a packet of seeds and a particle
6
+ # accelerator... backwards.
7
+ #
8
+ module PG::EM::Client::Helper
9
+ # Generate SQL for an insert statement into `tbl`, with the fields and
10
+ # data given by the keys and values, respectively, of `params`. Returns
11
+ # a two-element array consisting of the parameterised SQL as the first
12
+ # element, and the array of parameters as the second element.
13
+ #
14
+ def insert_sql(tbl, params)
15
+ keys = params.keys.map { |k| quote_identifier(k.to_s) }.join(',')
16
+ vals = params.values
17
+ val_places = (1..vals.length).to_a.map { |i| "$#{i}" }.join(',')
18
+
19
+ ["INSERT INTO #{quote_identifier(tbl)} (#{keys}) VALUES (#{val_places})", vals]
20
+ end
21
+
22
+ # Run an insert query, without having to write a great pile of SQL all by
23
+ # yourself.
24
+ #
25
+ # Arguments:
26
+ #
27
+ # * `db` -- A PG::EM::Client or PG::EM::ConnectionPool instance, against
28
+ # which all database operations will be executed.
29
+ #
30
+ # * `tbl` -- The name of the table into which you wish to insert your data.
31
+ # This parameter will be automatically quoted, if necessary.
32
+ #
33
+ # * `params` -- A hash containing the fields you wish to insert into
34
+ # (the keys of the hash) and the values to insert into each field (the
35
+ # values of the hash). All field names and data will be automatically
36
+ # quoted and made safe, so you're automatically SQL injection-proof!
37
+ #
38
+ # This method returns the deferrable in which the query is being called;
39
+ # this means you should attach the code to run after the query completes
40
+ # with `#callback`, and you can attach an error handler with `#errback`
41
+ # if you like.
42
+ #
43
+ def db_insert(db, tbl, params)
44
+ db.exec_defer(*insert_sql(tbl, params))
45
+ end
46
+
47
+ # Execute code in a transaction.
48
+ #
49
+ # Arguments:
50
+ #
51
+ # * `db` -- A PG::EM::Client or PG::EM::ConnectionPool instance, against
52
+ # which all database operations will be executed. If you pass a
53
+ # ConnectionPool, we will automatically hold a single connection for
54
+ # the transaction to complete against, so you don't have to worry
55
+ # about that, either.
56
+ #
57
+ # * `blk` -- A block of code which will be executed within the context
58
+ # of the transaction. This block will be passed a
59
+ # `PG::EM::Client::Helper::Transaction` instance, which has methods to
60
+ # allow you to commit or rollback the transaction, and execute SQL
61
+ # statements within the context of the transaction.
62
+ #
63
+ # Returns a deferrable object, on which you can call `#callback` and
64
+ # `#errback` to define what to do when the transaction succeeds or fails,
65
+ # respectively.
66
+ #
67
+ def db_transaction(db, opts = {}, &blk)
68
+ if db.is_a? PG::EM::ConnectionPool
69
+ db.__send__(:hold_deferrable) do |conn|
70
+ ::PG::EM::Client::Helper::Transaction.new(conn, opts, &blk)
71
+ end
72
+ else
73
+ ::PG::EM::Client::Helper::Transaction.new(db, opts, &blk)
74
+ end
75
+ end
76
+
77
+ # Take a PgSQL identifier (anything that isn't data, basically) and quote
78
+ # it so that it will always be valid, no matter what insanity someone's
79
+ # decided to put in their names.
80
+ #
81
+ def quote_identifier(id)
82
+ '"' + id.gsub(/"/, '""') + '"'
83
+ end
84
+
85
+ class Transaction
86
+ include ::PG::EM::Client::Helper
87
+ include ::EventMachine::Deferrable
88
+
89
+ # Create a new transaction. You shouldn't have to call this yourself;
90
+ # `db_transaction` should create one and pass it to your block.
91
+ def initialize(conn, opts, &blk)
92
+ @conn = conn
93
+ @opts = opts
94
+ @active = true
95
+
96
+ conn.exec_defer("BEGIN").callback do
97
+ blk.call(self)
98
+ commit if @active
99
+ end.errback { |ex| rollback(ex) }
100
+ end
101
+
102
+ # Signal the database to commit this transaction. You must do this
103
+ # once you've completed your queries, it won't be called automatically
104
+ # for you. Once you've committed the transaction, you cannot use it
105
+ # again; if you execute a query against a committed transaction, an
106
+ # exception will be raised.
107
+ #
108
+ def commit
109
+ @conn.exec_defer("COMMIT").callback do
110
+ self.succeed
111
+ @active = false
112
+ end.errback { |ex| rollback(ex) }
113
+ end
114
+
115
+ # Signal the database to abort this transaction. You only need to
116
+ # call this method if you need to rollback for some business logic
117
+ # reason -- a rollback will be automatically performed for you in the
118
+ # event of a database error or other exception.
119
+ #
120
+ def rollback(ex)
121
+ @conn.exec_defer("ROLLBACK") do
122
+ @active = false
123
+ self.fail(ex)
124
+ end
125
+ end
126
+
127
+ # Insert a row of data into the database table `tbl`, using the keys
128
+ # from the `params` hash as the field names, and the values from the
129
+ # `params` hash as the field data. Once the query has completed,
130
+ # `blk` will be called to allow the transaction to continue.
131
+ #
132
+ def insert(tbl, params, &blk)
133
+ exec(*insert_sql(tbl, params), &blk)
134
+ end
135
+
136
+ # Execute an arbitrary block of SQL in `sql` within the transaction.
137
+ # If you need to pass dynamic values to the query, those should be
138
+ # given in `values`, and referenced in `sql` as `$1`, `$2`, etc. The
139
+ # given block will be called if and when the query completes
140
+ # successfully.
141
+ #
142
+ def exec(sql, values=[], &blk)
143
+ df = @conn.exec_defer(sql, values).
144
+ errback { |ex| rollback(ex) }
145
+ df.callback(&blk) if blk
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,14 @@
1
+ require_relative './spec_helper'
2
+
3
+ describe "PG::EM::Client::Helper#db_insert" do
4
+ it "Calls the right SQL" do
5
+ expect(db = double).
6
+ to receive(:exec_defer).
7
+ with(
8
+ 'INSERT INTO "foo" ("bar") VALUES ($1)',
9
+ ['baz']
10
+ )
11
+
12
+ db_insert(db, "foo", :bar => "baz")
13
+ end
14
+ end
@@ -0,0 +1,76 @@
1
+ require_relative './spec_helper'
2
+
3
+ describe "PG::EM::Client::Helper#db_transaction" do
4
+ def mock_query_chain(queries, fail_on = -1, &blk)
5
+ mock_conn = double(PG::EM::Client)
6
+
7
+ queries.each_with_index do |q, i|
8
+ df = EM::DefaultDeferrable.new
9
+
10
+ ex = expect(mock_conn).
11
+ to receive(:exec_defer).
12
+ with(*q).
13
+ and_return(df)
14
+
15
+ # Rollback expects a yield
16
+ if q == ["ROLLBACK"]
17
+ ex.and_yield()
18
+ end
19
+
20
+ if i == fail_on
21
+ df.fail
22
+ else
23
+ df.succeed
24
+ end
25
+ end
26
+
27
+ EM.run_block do
28
+ db_transaction(mock_conn, &blk)
29
+ end
30
+ end
31
+
32
+ it "runs a BEGIN/COMMIT cycle by default" do
33
+ mock_query_chain([["BEGIN"], ["COMMIT"]]) do
34
+ # Nothing
35
+ end
36
+ end
37
+
38
+ it "rolls back if BEGIN fails" do
39
+ mock_query_chain([["BEGIN"], ["ROLLBACK"]], 0) do
40
+ # Nothing
41
+ end
42
+ end
43
+
44
+ it "rolls back if COMMIT fails" do
45
+ mock_query_chain([["BEGIN"], ["COMMIT"], ["ROLLBACK"]], 1) do
46
+ # Nothing
47
+ end
48
+ end
49
+
50
+ it "runs a simple INSERT" do
51
+ mock_query_chain([
52
+ ["BEGIN"],
53
+ ['INSERT INTO "foo" ("bar") VALUES ($1)',
54
+ ["baz"]
55
+ ],
56
+ ["COMMIT"]
57
+ ]) do |txn|
58
+ txn.insert("foo", :bar => 'baz')
59
+ end
60
+ end
61
+
62
+ it "rolls back after a failed INSERT" do
63
+ mock_query_chain(
64
+ [
65
+ ["BEGIN"],
66
+ ['INSERT INTO "foo" ("bar") VALUES ($1)',
67
+ ["baz"]
68
+ ],
69
+ ["ROLLBACK"]
70
+ ],
71
+ 1
72
+ ) do |txn|
73
+ txn.insert("foo", :bar => 'baz')
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,11 @@
1
+ require_relative './spec_helper'
2
+
3
+ describe "PG::EM::Client::Helper#insert_sql" do
4
+ it "assembles a simple query correctly" do
5
+ expect(insert_sql("foo", :bar => "baz", :wombat => 42)).
6
+ to eq([
7
+ 'INSERT INTO "foo" ("bar","wombat") VALUES ($1,$2)',
8
+ ["baz", 42]
9
+ ])
10
+ end
11
+ end
@@ -0,0 +1,27 @@
1
+ require 'spork'
2
+
3
+ Spork.prefork do
4
+ require 'bundler'
5
+ Bundler.setup(:default, :test)
6
+ require 'rspec/core'
7
+ require 'rspec/mocks'
8
+
9
+ require 'pry'
10
+
11
+ RSpec.configure do |config|
12
+ config.fail_fast = true
13
+ # config.full_backtrace = true
14
+
15
+ config.expect_with :rspec do |c|
16
+ c.syntax = :expect
17
+ end
18
+ end
19
+ end
20
+
21
+ Spork.each_run do
22
+ require 'em-pg-client-helper'
23
+
24
+ RSpec.configure do |config|
25
+ config.include ::PG::EM::Client::Helper
26
+ end
27
+ end
metadata ADDED
@@ -0,0 +1,255 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: em-pg-client-helper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Matt Palmer
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-10-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: git-version-bump
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '0.10'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '0.10'
30
+ - !ruby/object:Gem::Dependency
31
+ name: em-pg-client
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '0.3'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '0.3'
46
+ - !ruby/object:Gem::Dependency
47
+ name: bundler
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: eventmachine
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: github-release
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: guard-spork
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: guard-rspec
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: rb-inotify
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ~>
132
+ - !ruby/object:Gem::Version
133
+ version: '0.9'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ~>
140
+ - !ruby/object:Gem::Version
141
+ version: '0.9'
142
+ - !ruby/object:Gem::Dependency
143
+ name: pry-debugger
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ! '>='
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ! '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ - !ruby/object:Gem::Dependency
159
+ name: rake
160
+ requirement: !ruby/object:Gem::Requirement
161
+ none: false
162
+ requirements:
163
+ - - ! '>='
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ! '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ - !ruby/object:Gem::Dependency
175
+ name: rdoc
176
+ requirement: !ruby/object:Gem::Requirement
177
+ none: false
178
+ requirements:
179
+ - - ! '>='
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
182
+ type: :development
183
+ prerelease: false
184
+ version_requirements: !ruby/object:Gem::Requirement
185
+ none: false
186
+ requirements:
187
+ - - ! '>='
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ - !ruby/object:Gem::Dependency
191
+ name: rspec
192
+ requirement: !ruby/object:Gem::Requirement
193
+ none: false
194
+ requirements:
195
+ - - ! '>='
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ type: :development
199
+ prerelease: false
200
+ version_requirements: !ruby/object:Gem::Requirement
201
+ none: false
202
+ requirements:
203
+ - - ! '>='
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
206
+ description:
207
+ email:
208
+ executables: []
209
+ extensions: []
210
+ extra_rdoc_files:
211
+ - README.md
212
+ files:
213
+ - .gitignore
214
+ - Gemfile
215
+ - Guardfile
216
+ - LICENCE
217
+ - README.md
218
+ - Rakefile
219
+ - em-pg-client-helper.gemspec
220
+ - lib/em-pg-client-helper.rb
221
+ - spec/db_insert_spec.rb
222
+ - spec/db_transaction_spec.rb
223
+ - spec/insert_sql_spec.rb
224
+ - spec/spec_helper.rb
225
+ homepage: http://github.com/mpalmer/em-pg-client-helper
226
+ licenses: []
227
+ post_install_message:
228
+ rdoc_options: []
229
+ require_paths:
230
+ - lib
231
+ required_ruby_version: !ruby/object:Gem::Requirement
232
+ none: false
233
+ requirements:
234
+ - - ! '>='
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
237
+ segments:
238
+ - 0
239
+ hash: -3158041732847442492
240
+ required_rubygems_version: !ruby/object:Gem::Requirement
241
+ none: false
242
+ requirements:
243
+ - - ! '>='
244
+ - !ruby/object:Gem::Version
245
+ version: '0'
246
+ segments:
247
+ - 0
248
+ hash: -3158041732847442492
249
+ requirements: []
250
+ rubyforge_project:
251
+ rubygems_version: 1.8.23
252
+ signing_key:
253
+ specification_version: 3
254
+ summary: Simplify common operations using em-pg-client
255
+ test_files: []