pg_helper 0.3.1 → 0.4.0

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: 2d1859b5d93f74a231783cba64bd601d701d0485
4
+ data.tar.gz: 500e32c9a63334b30a967e3b7b35054fca89ad32
5
+ SHA512:
6
+ metadata.gz: 1945e53defa8da6e57974514d567eeb9d3a0236db0eb26bd15b26d2d63c79ae3f7b173dd31a6488ce834418a9648cf7860179603e7ad1b2b9c89dc1076998602
7
+ data.tar.gz: a63b6e170f7bbfd367a5676758fd0fe1869202f25d9e5f95071d04cd9f9963a23f5a95d09a0b0e1846e47749a01e3fd149864181ae2417c25d477772914df5c5
@@ -0,0 +1 @@
1
+ 2.3.0
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source :rubygems
1
+ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in pg_helper.gemspec
4
4
  gemspec
data/HISTORY.md CHANGED
@@ -1,3 +1,6 @@
1
+ ## DEV
2
+ * QueryBuilder for cases where Arel is too much.
3
+
1
4
  ## 0.3.1 (2013.03)
2
5
  * It is possible to give existing connection to QueryHelper constructor
3
6
 
data/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  PgHelper
2
2
  =============
3
+ [![Build Status](https://travis-ci.org/webervin/pg_helper.png)]
4
+ (https://travis-ci.org/webervin/pg_helper)
5
+ [![Code Climate](https://codeclimate.com/github/webervin/pg_helper.png)](https://codeclimate.com/github/webervin/pg_helper)
6
+ [![Dependency Status](https://gemnasium.com/webervin/pg_helper.png)](https://gemnasium.com/webervin/pg_helper)
3
7
 
4
8
  Because sometimes I don't want ActiveRecord to parse all fields, nor think about connection.
5
9
  All features are actually provided by [Pg gem](http://rubygems.org/gems/pg)
@@ -1,7 +1,8 @@
1
-
2
- require 'rubygems'
1
+ # require 'rubygems' < - this will be required by package manager
3
2
  require 'pg'
4
3
 
5
4
  # require all of the library files
6
- # note, you may need to specify these explicitly if there are any load order dependencies
7
- require 'pg_helper/query_helper.rb'
5
+ # note, you may need to specify these explicitly if there are any
6
+ # load order dependencies
7
+ require 'pg_helper/support_classes'
8
+ require 'pg_helper/query_helper'
@@ -0,0 +1,361 @@
1
+ # This file is simplified version of ActiveRecord::Base::ConnectionPool
2
+ # (from Ruby on Rails framework).
3
+ #
4
+ # Original code is:
5
+ # https://github.com/rails
6
+ # Distributed under MIT license.
7
+ #
8
+ # Rails is developed and maintained by
9
+ # David Heinemeier Hansson
10
+ # http://www.rubyonrails.org
11
+ #
12
+ # Please note that as code is modified by me Ruby on Rails team is not responsible for any bugs,
13
+ # bugs are added by me :)
14
+ # Also note that pg_helper is NOT related to Ruby on Rails in any way
15
+
16
+ require 'thread'
17
+ require 'thread_safe'
18
+ require 'monitor'
19
+ require 'set'
20
+ require 'pg'
21
+
22
+ module PgHelper
23
+ # Raised when a connection could not be obtained within the connection
24
+ # acquisition timeout period: because max connections in pool
25
+ # are in use.
26
+ class CouldNotObtainConnection < RuntimeError
27
+ end
28
+
29
+ class ConnectionPool
30
+ # nearly standard queue, but with timeout on wait
31
+ # FIXME: custom class inherit from
32
+ # http://www.ruby-doc.org/stdlib-2.0/libdoc/thread/rdoc/Queue.html
33
+ class Queue
34
+ def initialize(lock = Monitor.new)
35
+ @lock = lock
36
+ @cond = @lock.new_cond
37
+ @num_waiting = 0
38
+ @queue = []
39
+ end
40
+
41
+ # Test if any threads are currently waiting on the queue.
42
+ def any_waiting?
43
+ synchronize do
44
+ @num_waiting > 0
45
+ end
46
+ end
47
+
48
+ # Return the number of threads currently waiting on this
49
+ # queue.
50
+ def num_waiting
51
+ synchronize do
52
+ $DEBUG && warn(@num_waiting.to_s)
53
+ @num_waiting
54
+ end
55
+ end
56
+
57
+ # Add +element+ to the queue. Never blocks.
58
+ def add(element)
59
+ synchronize do
60
+ @queue.push element
61
+ @cond.signal
62
+ end
63
+ end
64
+
65
+ # If +element+ is in the queue, remove and return it, or nil.
66
+ def delete(element)
67
+ synchronize do
68
+ @queue.delete(element)
69
+ end
70
+ end
71
+
72
+ # Remove all elements from the queue.
73
+ def clear
74
+ synchronize do
75
+ @queue.clear
76
+ end
77
+ end
78
+
79
+ # Remove the head of the queue.
80
+ #
81
+ # If +timeout+ is not given, remove and return the head the
82
+ # queue if the number of available elements is strictly
83
+ # greater than the number of threads currently waiting (that
84
+ # is, don't jump ahead in line). Otherwise, return nil.
85
+ #
86
+ # If +timeout+ is given, block if it there is no element
87
+ # available, waiting up to +timeout+ seconds for an element to
88
+ # become available.
89
+ #
90
+ # Raises:
91
+ # - ConnectionTimeoutError if +timeout+ is given and no element
92
+ # becomes available after +timeout+ seconds,
93
+ def poll(timeout = nil)
94
+ synchronize do
95
+ if timeout
96
+ no_wait_poll || wait_poll(timeout)
97
+ else
98
+ no_wait_poll
99
+ end
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def synchronize(&block)
106
+ @lock.synchronize(&block)
107
+ end
108
+
109
+ # Test if the queue currently contains any elements.
110
+ def any?
111
+ !@queue.empty?
112
+ end
113
+
114
+ # A thread can remove an element from the queue without
115
+ # waiting if an only if the number of currently available
116
+ # connections is strictly greater than the number of waiting
117
+ # threads.
118
+ def can_remove_no_wait?
119
+ @queue.size > @num_waiting
120
+ end
121
+
122
+ # Removes and returns the head of the queue if possible, or nil.
123
+ def remove
124
+ @queue.shift
125
+ end
126
+
127
+ # Remove and return the head the queue if the number of
128
+ # available elements is strictly greater than the number of
129
+ # threads currently waiting. Otherwise, return nil.
130
+ def no_wait_poll
131
+ remove if can_remove_no_wait?
132
+ end
133
+
134
+ # Waits on the queue up to +timeout+ seconds, then removes and
135
+ # returns the head of the queue.
136
+ def wait_poll(timeout)
137
+ @num_waiting += 1
138
+
139
+ t0 = Time.now
140
+ elapsed = 0
141
+ loop do
142
+ @cond.wait(timeout - elapsed)
143
+
144
+ return remove if any?
145
+
146
+ elapsed = Time.now - t0
147
+ if elapsed >= timeout
148
+ msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' %
149
+ [timeout, elapsed]
150
+ fail CouldNotObtainConnection, msg
151
+ end
152
+ end
153
+ ensure
154
+ @num_waiting -= 1
155
+ end
156
+ end
157
+
158
+ include MonitorMixin
159
+
160
+ attr_accessor :auto_connect, :checkout_timeout
161
+ attr_reader :connection_options, :connections, :size
162
+
163
+ # all opts but :checkout_timeout, :pool, :auto_connect will be passed to PGConn.new
164
+ def initialize(opts)
165
+ super()
166
+
167
+ connection_opts = opts.dup
168
+ @checkout_timeout = opts.delete(:checkout_timeout) || 5
169
+ @size = opts.delete(:pool) || 5
170
+ @auto_connect = opts.delete(:auto_connect) || true
171
+
172
+ @connection_options = connection_opts
173
+
174
+ # The cache of reserved connections mapped to threads
175
+ @reserved_connections = ThreadSafe::Cache.new(initial_capacity: @size)
176
+
177
+ @connections = []
178
+
179
+ @available = Queue.new self
180
+ end
181
+
182
+ # Retrieve the connection associated with the current thread, or call
183
+ # #checkout to obtain one if necessary.
184
+ #
185
+ # #connection can be called any number of times; the connection is
186
+ # held in a hash keyed by the thread id.
187
+ def connection
188
+ # this is correctly done double-checked locking
189
+ # (ThreadSafe::Cache's lookups have volatile semantics)
190
+ @reserved_connections[current_connection_id] || synchronize do
191
+ @reserved_connections[current_connection_id] ||= checkout
192
+ end
193
+ end
194
+
195
+ # Is there an open connection that is being used for the current thread?
196
+ def active_connection?
197
+ synchronize do
198
+ @reserved_connections.fetch(current_connection_id) do
199
+ return false
200
+ end
201
+ end
202
+ end
203
+
204
+ # Signal that the thread is finished with the current connection.
205
+ # #release_connection releases the connection-thread association
206
+ # and returns the connection to the pool.
207
+ def release_connection(with_id = current_connection_id)
208
+ synchronize do
209
+ conn = @reserved_connections.delete(with_id)
210
+ checkin conn if conn
211
+ end
212
+ end
213
+
214
+ # If a connection already exists yield it to the block. If no connection
215
+ # exists checkout a connection, yield it to the block, and checkin the
216
+ # connection when finished.
217
+ def with_connection
218
+ connection_id = current_connection_id
219
+ fresh_connection = true unless active_connection?
220
+ yield connection
221
+ ensure
222
+ release_connection(connection_id) if fresh_connection
223
+ end
224
+
225
+ # Returns true if a connection has already been opened.
226
+ def connected?
227
+ synchronize { @connections.any? }
228
+ end
229
+
230
+ # Disconnects all connections in the pool, and clears the pool.
231
+ def disconnect!
232
+ synchronize do
233
+ @reserved_connections.clear
234
+ @connections.each do |conn|
235
+ checkin conn
236
+ $DEBUG && warn("Closing pg connection: #{conn.object_id}")
237
+ conn.close
238
+ end
239
+ @connections = []
240
+ @available.clear
241
+ end
242
+ end
243
+
244
+ # Clears the cache which maps classes.
245
+ def clear_reloadable_connections!
246
+ synchronize do
247
+ @reserved_connections.clear
248
+ @connections.each do |conn|
249
+ checkin conn
250
+ end
251
+
252
+ @connections.delete_if(&:finished?)
253
+
254
+ @available.clear
255
+ @connections.each do |conn|
256
+ @available.add conn
257
+ end
258
+ end
259
+ end
260
+
261
+ # Check-out a database connection from the pool, indicating that you want
262
+ # to use it. You should call #checkin when you no longer need this.
263
+ #
264
+ # This is done by either returning and leasing existing connection, or by
265
+ # creating a new connection and leasing it.
266
+ #
267
+ # If all connections are leased and the pool is at capacity (meaning the
268
+ # number of currently leased connections is greater than or equal to the
269
+ # size limit set), an ActiveRecord::ConnectionTimeoutError exception will be raised.
270
+ #
271
+ # Returns: an AbstractAdapter object.
272
+ #
273
+ # Raises:
274
+ # - ConnectionTimeoutError: no connection can be obtained from the pool.
275
+ def checkout
276
+ synchronize do
277
+ conn = acquire_connection
278
+ checkout_and_verify(conn)
279
+ end
280
+ end
281
+
282
+ # Check-in a database connection back into the pool, indicating that you
283
+ # no longer need this connection.
284
+ #
285
+ # +conn+: an AbstractAdapter object, which was obtained by earlier by
286
+ # calling +checkout+ on this pool.
287
+ def checkin(conn)
288
+ synchronize do
289
+ release conn
290
+ @available.add conn
291
+ end
292
+ end
293
+ # # Remove a connection from the connection pool. The connection will
294
+ # # remain open and active but will no longer be managed by this pool.
295
+ # def remove(conn)
296
+ # synchronize do
297
+ # @connections.delete conn
298
+ # @available.delete conn
299
+ #
300
+ # # FIXME: we might want to store the key on the connection so that removing
301
+ # # from the reserved hash will be a little easier.
302
+ # release conn
303
+ #
304
+ # @available.add checkout_new_connection if @available.any_waiting?
305
+ # end
306
+ # end
307
+
308
+ private
309
+
310
+ # Acquire a connection by one of 1) immediately removing one
311
+ # from the queue of available connections, 2) creating a new
312
+ # connection if the pool is not at capacity, 3) waiting on the
313
+ # queue for a connection to become available.
314
+ #
315
+ # Raises:
316
+ # - ConnectionTimeoutError if a connection could not be acquired
317
+ def acquire_connection
318
+ if conn = @available.poll
319
+ conn
320
+ elsif @connections.size < @size
321
+ checkout_new_connection
322
+ else
323
+ @available.poll(@checkout_timeout)
324
+ end
325
+ end
326
+
327
+ def release(conn)
328
+ thread_id = if @reserved_connections[current_connection_id] == conn
329
+ current_connection_id
330
+ else
331
+ @reserved_connections.keys.find do |k|
332
+ @reserved_connections[k] == conn
333
+ end
334
+ end
335
+
336
+ @reserved_connections.delete thread_id if thread_id
337
+ end
338
+
339
+ def new_connection
340
+ conn = PGconn.open(connection_options)
341
+ $DEBUG && warn("Connected to PostgreSQL #{conn.server_version} (#{conn.object_id})")
342
+ conn
343
+ end
344
+
345
+ def current_connection_id #:nodoc:
346
+ Thread.current.object_id
347
+ end
348
+
349
+ def checkout_new_connection
350
+ fail ConnectionNotEstablished unless @auto_connect
351
+ c = new_connection
352
+ @connections << c
353
+ c
354
+ end
355
+
356
+ def checkout_and_verify(c)
357
+ c.reset
358
+ c
359
+ end
360
+ end
361
+ end
@@ -0,0 +1,80 @@
1
+ module PgHelper
2
+ class QueryBuilder
3
+ attr_reader :table_name
4
+
5
+ def self.from(table_name)
6
+ new(table_name)
7
+ end
8
+
9
+ def initialize(table_name)
10
+ @table_name = table_name
11
+ @selects = []
12
+ @where = []
13
+ @cte_list = []
14
+ @join_list = []
15
+ end
16
+
17
+ # http://www.postgresql.org/docs/9.2/static/sql-select.html
18
+ def to_sql
19
+ "#{with_list}"\
20
+ "SELECT #{column_list} "\
21
+ "FROM #{table_name}"\
22
+ "#{join_list}"\
23
+ "#{where_list}"
24
+ end
25
+
26
+ def select(value)
27
+ @selects << value
28
+ self
29
+ end
30
+
31
+ def where(condition)
32
+ @where << condition
33
+ self
34
+ end
35
+
36
+ def with(cte_name, cte_query)
37
+ @cte_list << "#{cte_name} AS (#{cte_query})"
38
+ self
39
+ end
40
+
41
+ def join(join_sql)
42
+ @join_list << join_sql
43
+ self
44
+ end
45
+
46
+ private
47
+
48
+ def join_list
49
+ if @join_list.empty?
50
+ nil
51
+ else
52
+ " #{@join_list.join(' ')}"
53
+ end
54
+ end
55
+
56
+ def with_list
57
+ if @cte_list.empty?
58
+ nil
59
+ else
60
+ "WITH #{@cte_list.join(',')} "
61
+ end
62
+ end
63
+
64
+ def where_list
65
+ if @where.empty?
66
+ nil
67
+ else
68
+ " WHERE #{@where.join(' AND ')}"
69
+ end
70
+ end
71
+
72
+ def column_list
73
+ if @selects.empty?
74
+ '*'
75
+ else
76
+ @selects.join(',')
77
+ end
78
+ end
79
+ end
80
+ end