github-ds 0.1.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,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