github-ds 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,229 @@
1
+ module GitHub
2
+ class Result
3
+ # Invokes the supplied block and wraps the return value in a
4
+ # GitHub::Result object.
5
+ #
6
+ # Exceptions raised by the block are caught and also wrapped.
7
+ #
8
+ # Example:
9
+ #
10
+ # GitHub::Result.new { 123 }
11
+ # # => #<GitHub::Result value: 123>
12
+ #
13
+ # GitHub::Result.new { raise "oops" }
14
+ # # => #<GitHub::Result error: #<RuntimeError: oops>>
15
+ #
16
+ def initialize
17
+ begin
18
+ @value = yield
19
+ @error = nil
20
+ rescue => e
21
+ @error = e
22
+ end
23
+ end
24
+
25
+ def to_s
26
+ if ok?
27
+ "#<GitHub::Result:0x%x value: %s>" % [object_id, @value.inspect]
28
+ else
29
+ "#<GitHub::Result:0x%x error: %s>" % [object_id, @error.inspect]
30
+ end
31
+ end
32
+
33
+ alias_method :inspect, :to_s
34
+
35
+ # If the result represents a value, invokes the supplied block with
36
+ # that value.
37
+ #
38
+ # If the result represents an error, returns self.
39
+ #
40
+ # The block must also return a GitHub::Result object.
41
+ # Use #map otherwise.
42
+ #
43
+ # Example:
44
+ #
45
+ # result = do_something().then { |val|
46
+ # do_other_thing(val)
47
+ # }
48
+ # # => #<GitHub::Result value: ...>
49
+ #
50
+ # do_something_that_fails().then { |val|
51
+ # # never invoked
52
+ # }
53
+ # # => #<GitHub::Result error: ...>
54
+ #
55
+ def then
56
+ if ok?
57
+ result = yield(@value)
58
+ raise TypeError, "block invoked in GitHub::Result#then did not return GitHub::Result" unless result.is_a?(Result)
59
+ result
60
+ else
61
+ self
62
+ end
63
+ end
64
+
65
+ # If the result represents an error, invokes the supplied block with that error.
66
+ #
67
+ # If the result represents a value, returns self.
68
+ #
69
+ # The block must also return a GitHub::Result object.
70
+ # Use #map otherwise.
71
+ #
72
+ # Example:
73
+ #
74
+ # result = do_something().rescue { |val|
75
+ # # never invoked
76
+ # }
77
+ # # => #<GitHub::Result value: ...>
78
+ #
79
+ # do_something_that_fails().rescue { |val|
80
+ # # handle_error(val)
81
+ # }
82
+ # # => #<GitHub::Result error: ...>
83
+ #
84
+ def rescue
85
+ return self if ok?
86
+ result = yield(@error)
87
+ raise TypeError, "block invoked in GitHub::Result#rescue did not return GitHub::Result" unless result.is_a?(Result)
88
+ result
89
+ end
90
+
91
+ # If the result represents a value, invokes the supplied block with that
92
+ # value and wraps the block's return value in a GitHub::Result.
93
+ #
94
+ # If the result represents an error, returns self.
95
+ #
96
+ # The block should not return a GitHub::Result object (unless you
97
+ # truly intend to create a GitHub::Result<GitHub::Result<T>>).
98
+ # Use #then if it does.
99
+ #
100
+ # Example:
101
+ #
102
+ # result = do_something()
103
+ # # => #<GitHub::Result value: 123>
104
+ #
105
+ # result.map { |val| val * 2 }
106
+ # # => #<GitHub::Result value: 246>
107
+ #
108
+ # do_something_that_fails().map { |val|
109
+ # # never invoked
110
+ # }
111
+ # # => #<GitHub::Result error: ...>
112
+ #
113
+ def map
114
+ if ok?
115
+ Result.new { yield(@value) }
116
+ else
117
+ self
118
+ end
119
+ end
120
+
121
+ # If the result represents a value, returns that value.
122
+ #
123
+ # If the result represents an error, invokes the supplied block with the
124
+ # exception object.
125
+ #
126
+ # Example:
127
+ #
128
+ # result = do_something()
129
+ # # => #<GitHub::Result value: "foo">
130
+ #
131
+ # result.value { "nope" }
132
+ # # => "foo"
133
+ #
134
+ # result = do_something_that_fails()
135
+ # # => #<GitHub::Result error: ...>
136
+ #
137
+ # result.value { "nope" }
138
+ # # => #<GitHub::Result value: "nope">
139
+ #
140
+ def value
141
+ unless block_given?
142
+ raise ArgumentError, "must provide a block to GitHub::Result#value to be invoked in case of error"
143
+ end
144
+
145
+ if ok?
146
+ @value
147
+ else
148
+ yield(@error)
149
+ end
150
+ end
151
+
152
+ # If the result represents a value, returns that value.
153
+ #
154
+ # If the result represents an error, raises that error.
155
+ #
156
+ # Example:
157
+ #
158
+ # result = do_something()
159
+ # # => #<GitHub::Result value: "foo">
160
+ #
161
+ # result.value!
162
+ # # => "foo"
163
+ #
164
+ # result = do_something_that_fails()
165
+ # # => #<GitHub::Result error: ...>
166
+ #
167
+ # result.value!
168
+ # # !! raises exception
169
+ #
170
+ def value!
171
+ if ok?
172
+ @value
173
+ else
174
+ raise @error
175
+ end
176
+ end
177
+
178
+ # Returns true if the result represents a value, false if an error.
179
+ #
180
+ # Example:
181
+ #
182
+ # result = do_something()
183
+ # # => #<GitHub::Result value: "foo">
184
+ #
185
+ # result.ok?
186
+ # # => true
187
+ #
188
+ # result = do_something_that_fails()
189
+ # # => #<GitHub::Result error: ...>
190
+ #
191
+ # result.ok?
192
+ # # => false
193
+ #
194
+ def ok?
195
+ !@error
196
+ end
197
+
198
+ # If the result represents a value, returns nil.
199
+ #
200
+ # If the result represents an error, returns that error.
201
+ #
202
+ # result = do_something()
203
+ # # => #<GitHub::Result value: "foo">
204
+ #
205
+ # result.error
206
+ # # => nil
207
+ #
208
+ # result = do_something_that_fails()
209
+ # # => #<GitHub::Result error: ...>
210
+ #
211
+ # result.error
212
+ # # => ...
213
+ #
214
+ def error
215
+ @error
216
+ end
217
+
218
+ # Create a GitHub::Result with only the error condition set.
219
+ #
220
+ # GitHub::Result.error(e)
221
+ # # => # <GitHub::Result error: ...>
222
+ #
223
+ def self.error(e)
224
+ result = allocate
225
+ result.instance_variable_set(:@error, e)
226
+ result
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,447 @@
1
+ require "active_record"
2
+
3
+ module GitHub
4
+ # Public: Build and execute a SQL query, returning results as Arrays. This
5
+ # class uses ActiveRecord's connection classes, but provides a better API for
6
+ # bind values and raw data access.
7
+ #
8
+ # Example:
9
+ #
10
+ # sql = GitHub::SQL.new(<<-SQL, :parent_ids => parent_ids, :network_id => network_id)
11
+ # SELECT * FROM repositories
12
+ # WHERE source_id = :network_id AND parent_id IN :parent_ids
13
+ # SQL
14
+ # sql.results
15
+ # => returns an Array of Arrays, one for each row
16
+ # sql.hash_results
17
+ # => returns an Array of Hashes instead
18
+ #
19
+ # Things to be aware of:
20
+ #
21
+ # * `nil` is always considered an error and not a usable value. If you need a
22
+ # SQL NULL, use the NULL constant instead.
23
+ #
24
+ # * Identical column names in SELECTs will be overridden:
25
+ # `SELECT t1.id, t2.id FROM...` will only return one value for `id`. To get
26
+ # more than one column of the same name, use aliases:
27
+ # `SELECT t1.id t1_id, t2.id t2_id FROM ...`
28
+ #
29
+ # * Arrays are escaped as `(item, item, item)`. If you need to insert multiple
30
+ # rows (Arrays of Arrays), you must specify the bind value using
31
+ # GitHub::SQL::ROWS(array_of_arrays).
32
+ #
33
+ class SQL
34
+
35
+ # Internal: a SQL literal value.
36
+ class Literal
37
+ # Public: the string value of this literal
38
+ attr_reader :value
39
+
40
+ def initialize(value)
41
+ @value = value.to_s.dup.freeze
42
+ end
43
+
44
+ def inspect
45
+ "<#{self.class.name} #{value}>"
46
+ end
47
+ end
48
+
49
+ # Internal: a list of arrays of values for insertion into SQL.
50
+ class Rows
51
+ # Public: the Array of row values
52
+ attr_reader :values
53
+
54
+ def initialize(values)
55
+ unless values.all? { |v| v.is_a? Array }
56
+ raise ArgumentError, "cannot instantiate SQL rows with anything but arrays"
57
+ end
58
+ @values = values.dup.freeze
59
+ end
60
+
61
+ def inspect
62
+ "<#{self.class.name} #{values.inspect}>"
63
+ end
64
+ end
65
+
66
+ # Public: Instantiate a literal SQL value.
67
+ #
68
+ # WARNING: The given value is LITERALLY inserted into your SQL without being
69
+ # escaped, so use this with extreme caution.
70
+ def self.LITERAL(string)
71
+ Literal.new(string)
72
+ end
73
+
74
+ # Public: Escape a binary SQL value
75
+ #
76
+ # Used when a column contains binary data which needs to be escaped
77
+ # to prevent warnings from MySQL
78
+ def self.BINARY(string)
79
+ GitHub::SQL.LITERAL(GitHub::SQL.BINARY_LITERAL(string))
80
+ end
81
+
82
+ # Public: Escape a binary SQL value, yielding a string which can be used as
83
+ # a literal in SQL
84
+ #
85
+ # Performs the core escaping logic for binary strings in MySQL
86
+ def self.BINARY_LITERAL(string)
87
+ "x'#{string.unpack("H*")[0]}'"
88
+ end
89
+
90
+ # Public: Instantiate a list of Arrays of SQL values for insertion.
91
+ def self.ROWS(rows)
92
+ Rows.new(rows)
93
+ end
94
+
95
+ # Public: prepackaged literal values.
96
+ NULL = Literal.new "NULL"
97
+ NOW = Literal.new "NOW()"
98
+
99
+ # Public: A superclass for errors.
100
+ class Error < RuntimeError
101
+ end
102
+
103
+ # Public: Raised when a bound ":keyword" value isn't available.
104
+ class BadBind < Error
105
+ def initialize(keyword)
106
+ super "There's no bind value for #{keyword.inspect}"
107
+ end
108
+ end
109
+
110
+ # Public: Raised when a bound value can't be sanitized.
111
+ class BadValue < Error
112
+ def initialize(value, description = nil)
113
+ description ||= "a #{value.class.name}"
114
+ super "Can't sanitize #{description}: #{value.inspect}"
115
+ end
116
+ end
117
+
118
+ # Internal: A Symbol-Keyed Hash of bind values.
119
+ attr_reader :binds
120
+
121
+ # Public: The SQL String to be executed. Modified in place.
122
+ attr_reader :query
123
+
124
+ # Public: Initialize a new instance.
125
+ #
126
+ # query - An initial SQL string (default: "").
127
+ # binds - A Hash of bind values keyed by Symbol (default: {}). There are
128
+ # a couple exceptions. If they clash with a bind value, add them
129
+ # in a later #bind or #add call.
130
+ #
131
+ # :connection - An ActiveRecord Connection adapter.
132
+ # :force_timezone - A Symbol describing the ActiveRecord default
133
+ # timezone. Either :utc or :local.
134
+ #
135
+ def initialize(query = nil, binds = nil)
136
+ if query.is_a? Hash
137
+ binds = query
138
+ query = nil
139
+ end
140
+
141
+ @last_insert_id = nil
142
+ @affected_rows = nil
143
+ @binds = binds ? binds.dup : {}
144
+ @query = ""
145
+ @connection = @binds.delete :connection
146
+ @force_tz = @binds.delete :force_timezone
147
+
148
+ add query if !query.nil?
149
+ end
150
+
151
+ # Public: Add a chunk of SQL to the query. Any ":keyword" tokens in the SQL
152
+ # will be replaced with database-safe values from the current binds.
153
+ #
154
+ # sql - A String containing a fragment of SQL.
155
+ # extras - A Hash of bind values keyed by Symbol (default: {}). These bind
156
+ # values are only be used to interpolate this SQL fragment,and
157
+ # aren't available to subsequent adds.
158
+ #
159
+ # Returns self.
160
+ # Raises GitHub::SQL::BadBind for unknown keyword tokens.
161
+ def add(sql, extras = nil)
162
+ return self if sql.blank?
163
+
164
+ query << " " unless query.empty?
165
+
166
+ if @force_tz
167
+ zone = ActiveRecord::Base.default_timezone
168
+ ActiveRecord::Base.default_timezone = @force_tz
169
+ end
170
+
171
+ query << interpolate(sql.strip, extras)
172
+
173
+ self
174
+ ensure
175
+ ActiveRecord::Base.default_timezone = zone if @force_tz
176
+ end
177
+
178
+ # Public: Add a chunk of SQL to the query, unless query generated so far is empty.
179
+ #
180
+ # Example: use this for conditionally adding UNION when generating sets of SELECTs.
181
+ #
182
+ # sql - A String containing a fragment of SQL.
183
+ # extras - A Hash of bind values keyed by Symbol (default: {}). These bind
184
+ # values are only be used to interpolate this SQL fragment,and
185
+ # aren't available to subsequent adds.
186
+ #
187
+ # Returns self.
188
+ # Raises GitHub::SQL::BadBind for unknown keyword tokens.
189
+ def add_unless_empty(sql, extras = nil)
190
+ return self if query.empty?
191
+ add sql, extras
192
+ end
193
+
194
+ # Public: The number of affected rows for this connection.
195
+ def affected_rows
196
+ @affected_rows || connection.raw_connection.affected_rows
197
+ end
198
+
199
+ # Public: Add additional bind values to be interpolated each time SQL
200
+ # is added to the query.
201
+ #
202
+ # hash - A Symbol-keyed Hash of new values.
203
+ #
204
+ # Returns self.
205
+ def bind(binds)
206
+ self.binds.merge! binds
207
+ self
208
+ end
209
+
210
+ # Internal: The object we use to execute SQL and retrieve results. Defaults
211
+ # to AR::B.connection, but can be overridden with a ":connection" key when
212
+ # initializing a new instance.
213
+ def connection
214
+ @connection || ActiveRecord::Base.connection
215
+ end
216
+
217
+ # Public: the number of rows found by the query.
218
+ #
219
+ # Returns FOUND_ROWS() if a SELECT query included SQL_CALC_FOUND_ROWS.
220
+ # Raises if SQL_CALC_FOUND_ROWS was not present in the query.
221
+ def found_rows
222
+ raise "no SQL_CALC_FOUND_ROWS clause present" unless defined? @found_rows
223
+ @found_rows
224
+ end
225
+
226
+ # Internal: Replace ":keywords" with sanitized values from binds or extras.
227
+ def interpolate(sql, extras = nil)
228
+ sql.gsub(/:[a-z][a-z0-9_]*/) do |raw|
229
+ sym = raw[1..-1].intern # O.o gensym
230
+
231
+ if extras && extras.include?(sym)
232
+ val = extras[sym]
233
+ elsif binds.include?(sym)
234
+ val = binds[sym]
235
+ end
236
+
237
+ raise BadBind.new raw if val.nil?
238
+
239
+ sanitize val
240
+ end
241
+ end
242
+
243
+ # Public: The last inserted ID for this connection.
244
+ def last_insert_id
245
+ @last_insert_id || connection.raw_connection.last_insert_id
246
+ end
247
+
248
+ # Public: Map each row to an instance of an ActiveRecord::Base subclass.
249
+ def models(klass)
250
+ return @models if defined? @models
251
+ return [] if frozen?
252
+
253
+ # Use select_all to retrieve hashes for each row instead of arrays of values.
254
+ @models = connection.
255
+ select_all(query, "#{klass.name} Load via #{self.class.name}").
256
+ collect! { |record| klass.send :instantiate, record }
257
+
258
+ retrieve_found_row_count
259
+ freeze
260
+
261
+ @models
262
+ end
263
+
264
+ # Public: Execute, memoize, and return the results of this query.
265
+ def results
266
+ return @results if defined? @results
267
+ return [] if frozen?
268
+
269
+ if @force_tz
270
+ zone = ActiveRecord::Base.default_timezone
271
+ ActiveRecord::Base.default_timezone = @force_tz
272
+ end
273
+
274
+ case query
275
+ when /\ADELETE/i
276
+ @affected_rows = connection.delete(query, "#{self.class.name} Delete")
277
+
278
+ when /\AINSERT/i
279
+ @last_insert_id = connection.insert(query, "#{self.class.name} Insert")
280
+
281
+ when /\AUPDATE/i
282
+ @affected_rows = connection.update(query, "#{self.class.name} Update")
283
+
284
+ when /\ASELECT/i
285
+ # Why not execute or select_rows? Because select_all hits the query cache.
286
+ @hash_results = connection.select_all(query, "#{self.class.name} Select")
287
+ @results = @hash_results.map(&:values)
288
+
289
+ else
290
+ @results = connection.execute(query, "#{self.class.name} Execute").to_a
291
+ end
292
+
293
+ @results ||= []
294
+
295
+ retrieve_found_row_count
296
+ freeze
297
+
298
+ @results
299
+ ensure
300
+ ActiveRecord::Base.default_timezone = zone if @force_tz
301
+ end
302
+
303
+ # Public: If the query is a SELECT, return an array of hashes instead of an array of arrays.
304
+ def hash_results
305
+ results
306
+ @hash_results || @results
307
+ end
308
+
309
+ # Public: Get first row of results.
310
+ def row
311
+ results.first
312
+ end
313
+
314
+ # Public: Execute, ignoring results. This is useful when the results of a
315
+ # query aren't important, often INSERTs, UPDATEs, or DELETEs.
316
+ #
317
+ # sql - An optional SQL string. See GitHub::SQL#add for details.
318
+ # extras - Optional bind values. See GitHub::SQL#add for details.
319
+ #
320
+ # Returns self.
321
+ def run(sql = nil, extras = nil)
322
+ add sql, extras if !sql.nil?
323
+ results
324
+
325
+ self
326
+ end
327
+
328
+ # Internal: when a SQL_CALC_FOUND_ROWS clause is present in a SELECT query,
329
+ # retrieve the FOUND_ROWS() value to get a count of the rows sans any
330
+ # LIMIT/OFFSET clause.
331
+ def retrieve_found_row_count
332
+ if query =~ /\A\s*SELECT\s+SQL_CALC_FOUND_ROWS\s+/i
333
+ @found_rows = connection.select_value "SELECT FOUND_ROWS()", self.class.name
334
+ end
335
+ end
336
+
337
+ # Public: Create and execute a new SQL query, ignoring results.
338
+ #
339
+ # sql - A SQL string. See GitHub::SQL#add for details.
340
+ # bindings - Optional bind values. See GitHub::SQL#add for details.
341
+ #
342
+ # Returns self.
343
+ def self.run(sql, bindings = {})
344
+ new(sql, bindings).run
345
+ end
346
+
347
+ # Public: Create and execute a new SQL query, returning its hash_result rows.
348
+ #
349
+ # sql - A SQL string. See GitHub::SQL#add for details.
350
+ # bindings - Optional bind values. See GitHub::SQL#add for details.
351
+ #
352
+ # Returns an Array of result hashes.
353
+ def self.hash_results(sql, bindings = {})
354
+ new(sql, bindings).hash_results
355
+ end
356
+
357
+ # Public: Create and execute a new SQL query, returning its result rows.
358
+ #
359
+ # sql - A SQL string. See GitHub::SQL#add for details.
360
+ # bindings - Optional bind values. See GitHub::SQL#add for details.
361
+ #
362
+ # Returns an Array of result arrays.
363
+ def self.results(sql, bindings = {})
364
+ new(sql, bindings).results
365
+ end
366
+
367
+ # Public: Create and execute a new SQL query, returning the value of the
368
+ # first column of the first result row.
369
+ #
370
+ # sql - A SQL string. See GitHub::SQL#add for details.
371
+ # bindings - Optional bind values. See GitHub::SQL#add for details.
372
+ #
373
+ # Returns a value or nil.
374
+ def self.value(sql, bindings = {})
375
+ new(sql, bindings).value
376
+ end
377
+
378
+ # Public: Create and execute a new SQL query, returning its values.
379
+ #
380
+ # sql - A SQL string. See GitHub::SQL#add for details.
381
+ # bindings - Optional bind values. See GitHub::SQL#add for details.
382
+ #
383
+ # Returns an Array of values.
384
+ def self.values(sql, bindings = {})
385
+ new(sql, bindings).values
386
+ end
387
+
388
+ # Internal: Make `value` database-safe. Ish.
389
+ def sanitize(value)
390
+ case value
391
+
392
+ when Integer
393
+ value.to_s
394
+
395
+ when Numeric, String
396
+ connection.quote value
397
+
398
+ when Array
399
+ raise BadValue.new(value, "an empty array") if value.empty?
400
+ raise BadValue.new(value, "a nested array") if value.any? { |v| v.is_a? Array }
401
+
402
+ "(" + value.map { |v| sanitize v }.join(", ") + ")"
403
+
404
+ when Literal
405
+ value.value
406
+
407
+ when Rows # rows for insertion
408
+ value.values.map { |v| sanitize v }.join(", ")
409
+
410
+ when Class
411
+ connection.quote value.name
412
+
413
+ when DateTime, Time, Date
414
+ connection.quote value.to_s(:db)
415
+
416
+ when true
417
+ connection.quoted_true
418
+
419
+ when false
420
+ connection.quoted_false
421
+
422
+ when Symbol
423
+ connection.quote value.to_s
424
+
425
+ else
426
+ raise BadValue, value
427
+ end
428
+ end
429
+
430
+ # Public: Get the first column of the first row of results.
431
+ def value
432
+ row && row.first
433
+ end
434
+
435
+ # Public: Is there a value?
436
+ def value?
437
+ !value.nil?
438
+ end
439
+
440
+ # Public: Get first column of every row of results.
441
+ #
442
+ # Returns an Array or nil.
443
+ def values
444
+ results.map(&:first)
445
+ end
446
+ end
447
+ end