sqb 1.0.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.
- checksums.yaml +7 -0
- data/lib/sqb/error.rb +4 -0
- data/lib/sqb/query.rb +346 -0
- data/lib/sqb/version.rb +3 -0
- data/lib/sqb.rb +2 -0
- metadata +48 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 44f58ba5485e199e6364aa99556e1a7b2ea8bb5520ff6f5059bff7dcc96df8c2
|
4
|
+
data.tar.gz: 8ae30d22f80255a79f13e0a372c65e62666202ec1c85f8a270a1bba75da96c99
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2d4bfa140701dfa6de3528106148c238e9be0e025ed11a1c857b9c795e54734491a3d582f8aed3ab2b73c0aec14c00e4044aca92aa9b180d5f721388fd9bacb8
|
7
|
+
data.tar.gz: 13405cee8bf220248ae53c9d6eb8aac63d142cbb7a9b9f8f33cccbed95b89857d0f31312b858ed57aebc5eeca7b03bbeca3d75abc02a266430f3730503b8276b
|
data/lib/sqb/error.rb
ADDED
data/lib/sqb/query.rb
ADDED
@@ -0,0 +1,346 @@
|
|
1
|
+
require 'sqb/error'
|
2
|
+
|
3
|
+
module SQB
|
4
|
+
class Query
|
5
|
+
|
6
|
+
VALID_ORDERS = ['ASC', 'DESC']
|
7
|
+
|
8
|
+
def initialize(table_name, &escape_block)
|
9
|
+
@table_name = table_name
|
10
|
+
@columns = []
|
11
|
+
@joins = []
|
12
|
+
@joins_name_mapping = {}
|
13
|
+
@where = []
|
14
|
+
@orders = []
|
15
|
+
@groups = []
|
16
|
+
@limit = nil
|
17
|
+
@offset = nil
|
18
|
+
@distinct = false
|
19
|
+
@where_within_or = []
|
20
|
+
@escape_block = escape_block
|
21
|
+
end
|
22
|
+
|
23
|
+
# Generate the full SQL query for this query.
|
24
|
+
#
|
25
|
+
# @return [String]
|
26
|
+
def to_sql
|
27
|
+
[].tap do |query|
|
28
|
+
query << "SELECT"
|
29
|
+
query << "DISTINCT" if @distinct
|
30
|
+
if @columns.empty?
|
31
|
+
query << column_tuple(@table_name, '*')
|
32
|
+
else
|
33
|
+
query << @columns.join(', ')
|
34
|
+
end
|
35
|
+
query << "FROM"
|
36
|
+
query << escape(@table_name)
|
37
|
+
|
38
|
+
unless @joins.empty?
|
39
|
+
query << @joins.join(' ')
|
40
|
+
end
|
41
|
+
|
42
|
+
unless @where.empty?
|
43
|
+
query << "WHERE"
|
44
|
+
query << @where.join(' AND ')
|
45
|
+
end
|
46
|
+
|
47
|
+
unless @groups.empty?
|
48
|
+
query << "GROUP BY"
|
49
|
+
query << @groups.join(', ')
|
50
|
+
end
|
51
|
+
|
52
|
+
unless @orders.empty?
|
53
|
+
query << "ORDER BY"
|
54
|
+
query << @orders.join(', ')
|
55
|
+
end
|
56
|
+
|
57
|
+
if @limit
|
58
|
+
query << "LIMIT #{@limit.to_i}"
|
59
|
+
end
|
60
|
+
|
61
|
+
if @offset
|
62
|
+
query << "OFFSET #{@offset.to_i}"
|
63
|
+
end
|
64
|
+
end.join(' ')
|
65
|
+
end
|
66
|
+
|
67
|
+
# Add a column to the query
|
68
|
+
#
|
69
|
+
# @param column [String, Symbol, Hash] the column name (or a hash with table & column name)
|
70
|
+
# @option options [String] :function a function to wrap around the column
|
71
|
+
# @options options [String] :as the name to return this column as
|
72
|
+
# @return [Query] returns the query
|
73
|
+
def column(column, options = {})
|
74
|
+
with_table_and_column(column) do |table, column|
|
75
|
+
@columns << [].tap do |query|
|
76
|
+
if options[:function]
|
77
|
+
query << "#{escape_function(options[:function])}("
|
78
|
+
end
|
79
|
+
query << column_tuple(table, column)
|
80
|
+
if options[:function]
|
81
|
+
query << ")"
|
82
|
+
end
|
83
|
+
if options[:as]
|
84
|
+
query << "AS"
|
85
|
+
query << escape(options[:as])
|
86
|
+
end
|
87
|
+
end.join(' ')
|
88
|
+
end
|
89
|
+
self
|
90
|
+
end
|
91
|
+
|
92
|
+
# Replace all existing columns with the given column
|
93
|
+
def column!(*args)
|
94
|
+
@columns = []
|
95
|
+
column(*args)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Add a condition to the query by providing a hash of keys and values.
|
99
|
+
#
|
100
|
+
# @param hash [Hash]
|
101
|
+
# @return [Query]
|
102
|
+
def where(hash)
|
103
|
+
if @where_within_or.last
|
104
|
+
@where_within_or.last << hash
|
105
|
+
else
|
106
|
+
@where << hash_to_sql(hash, @table_name)
|
107
|
+
end
|
108
|
+
self
|
109
|
+
end
|
110
|
+
|
111
|
+
# Set that all conditions added in this block should be joined using OR
|
112
|
+
# rather than AND.
|
113
|
+
def or(&block)
|
114
|
+
# Start by making an array within the OR block for this calling
|
115
|
+
@where_within_or << []
|
116
|
+
# Execute the block. All queries to 'where' will be added to the last
|
117
|
+
# array in the chain (created above)
|
118
|
+
block.call
|
119
|
+
ensure
|
120
|
+
# Start work on a full array of SQL fragments for all OR queries
|
121
|
+
@where_within_or_sql ||= []
|
122
|
+
# After each OR call, store up the SQL fragment for all where queries
|
123
|
+
# executed within the block.
|
124
|
+
if w = @where_within_or.pop
|
125
|
+
@where_within_or_sql << w.map do |w|
|
126
|
+
hash_to_sql(w, @table_name)
|
127
|
+
end.join(' OR ')
|
128
|
+
end
|
129
|
+
|
130
|
+
# When there are no fragments in the chain left, add it to the main
|
131
|
+
# where chain for the query.
|
132
|
+
if @where_within_or.empty?
|
133
|
+
@where << "(#{@where_within_or_sql.flatten.join(' OR ')})"
|
134
|
+
@where_within_or_sql = nil
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Limit the number of records return
|
139
|
+
#
|
140
|
+
# @param number [Integer]
|
141
|
+
# @return [Query]
|
142
|
+
def limit(number)
|
143
|
+
@limit = number&.to_i
|
144
|
+
self
|
145
|
+
end
|
146
|
+
|
147
|
+
# Set the offset
|
148
|
+
#
|
149
|
+
# @param number [Integer]
|
150
|
+
# @return [Query]
|
151
|
+
def offset(number)
|
152
|
+
@offset = number&.to_i
|
153
|
+
self
|
154
|
+
end
|
155
|
+
|
156
|
+
# Add an order column
|
157
|
+
#
|
158
|
+
# @param column [String, Symbol, Hash]
|
159
|
+
# @param direction [String] 'ASC' or 'DESC' (default 'ASC')
|
160
|
+
# @return [Query]
|
161
|
+
def order(column, direction = nil)
|
162
|
+
direction = direction ? direction.to_s.upcase : 'ASC'
|
163
|
+
|
164
|
+
unless VALID_ORDERS.include?(direction)
|
165
|
+
raise Error, "Invalid order direction #{direction}"
|
166
|
+
end
|
167
|
+
|
168
|
+
with_table_and_column(column) do |table, column|
|
169
|
+
@orders << [column_tuple(table, column), direction].join(' ')
|
170
|
+
end
|
171
|
+
|
172
|
+
self
|
173
|
+
end
|
174
|
+
|
175
|
+
# Add an order replacing all previous ones
|
176
|
+
def order!(*args)
|
177
|
+
@orders = []
|
178
|
+
order(*args)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Remove all ordering for this query
|
182
|
+
def no_order!
|
183
|
+
@orders = []
|
184
|
+
end
|
185
|
+
|
186
|
+
# Add a grouping
|
187
|
+
#
|
188
|
+
# @param column [String, Symbol, Hash]
|
189
|
+
# @return [Query]
|
190
|
+
def group_by(column)
|
191
|
+
with_table_and_column(column) do |table, column|
|
192
|
+
@groups << column_tuple(table, column)
|
193
|
+
end
|
194
|
+
self
|
195
|
+
end
|
196
|
+
|
197
|
+
# Add a join
|
198
|
+
#
|
199
|
+
# @param table_name [String, Symbol]
|
200
|
+
# @param foreign_key [String, Symbol]
|
201
|
+
# @option options [Hash] :where
|
202
|
+
# @option options [Array] :select
|
203
|
+
# @return [Query]
|
204
|
+
def join(table_name, foreign_key, options = {})
|
205
|
+
|
206
|
+
if options[:name]
|
207
|
+
join_name = options[:name]
|
208
|
+
else
|
209
|
+
@joins_name_mapping[table_name] ||= 0
|
210
|
+
join_name= "#{table_name}_#{@joins_name_mapping[table_name]}"
|
211
|
+
@joins_name_mapping[table_name] += 1
|
212
|
+
end
|
213
|
+
|
214
|
+
@joins << [].tap do |query|
|
215
|
+
query << "INNER JOIN"
|
216
|
+
query << escape(table_name)
|
217
|
+
query << "AS"
|
218
|
+
query << escape(join_name)
|
219
|
+
query << "ON"
|
220
|
+
query << column_tuple(@table_name, 'id')
|
221
|
+
query << "="
|
222
|
+
query << column_tuple(join_name, foreign_key)
|
223
|
+
end.join(' ')
|
224
|
+
|
225
|
+
if options[:where]
|
226
|
+
join_where = options[:where].each_with_object({}) do |(column, value), hash|
|
227
|
+
hash[{join_name => column}] = value
|
228
|
+
end
|
229
|
+
where(join_where)
|
230
|
+
end
|
231
|
+
|
232
|
+
if columns = options[:columns]
|
233
|
+
for field in columns
|
234
|
+
column({join_name => field}, :as => "#{join_name}_#{field}")
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
if g = options[:group_by]
|
239
|
+
group_by(join_name => g.is_a?(Symbol) ? g : :id)
|
240
|
+
end
|
241
|
+
|
242
|
+
self
|
243
|
+
end
|
244
|
+
|
245
|
+
def distinct
|
246
|
+
@distinct = true
|
247
|
+
self
|
248
|
+
end
|
249
|
+
|
250
|
+
private
|
251
|
+
|
252
|
+
def hash_to_sql(hash, table, joiner = ' AND ')
|
253
|
+
sql = hash.map do |key, value|
|
254
|
+
if key.is_a?(Hash)
|
255
|
+
table = key.first[0]
|
256
|
+
key = key.first[1]
|
257
|
+
end
|
258
|
+
|
259
|
+
key = column_tuple(table, key)
|
260
|
+
|
261
|
+
if value.is_a?(Array)
|
262
|
+
escaped_values = value.map { |v| value_escape(v) }.join(', ')
|
263
|
+
"#{key} IN (#{escaped_values})"
|
264
|
+
elsif value.is_a?(Hash)
|
265
|
+
sql = []
|
266
|
+
value.each do |operator, value|
|
267
|
+
case operator
|
268
|
+
when :not_equal
|
269
|
+
if value.nil?
|
270
|
+
sql << "#{key} IS NOT NULL"
|
271
|
+
else
|
272
|
+
sql << "#{key} != #{value_escape(value)}"
|
273
|
+
end
|
274
|
+
when :equal
|
275
|
+
if value.nil?
|
276
|
+
sql << "#{key} IS NULL"
|
277
|
+
else
|
278
|
+
sql << "#{key} = #{value_escape(value)}"
|
279
|
+
end
|
280
|
+
when :less_than
|
281
|
+
sql << "#{key} < #{value_escape(value)}"
|
282
|
+
when :greater_than
|
283
|
+
sql << "#{key} > #{value_escape(value)}"
|
284
|
+
when :less_than_or_equal_to
|
285
|
+
sql << "#{key} <= #{value_escape(value)}"
|
286
|
+
when :greater_than_or_equal_to
|
287
|
+
sql << "#{key} >= #{value_escape(value)}"
|
288
|
+
when :in, :not_in
|
289
|
+
escaped_values = value.map { |v| value_escape(v) }.join(', ')
|
290
|
+
op = operator == :in ? "IN" : "NOT IN"
|
291
|
+
sql << "#{key} #{op} (#{escaped_values})"
|
292
|
+
else
|
293
|
+
raise Error, "Invalid operator '#{operator}'"
|
294
|
+
end
|
295
|
+
end
|
296
|
+
sql.empty? ? "1=0" : sql.join(joiner)
|
297
|
+
elsif value == nil
|
298
|
+
"#{key} IS NULL"
|
299
|
+
else
|
300
|
+
"#{key} = #{value_escape(value)}"
|
301
|
+
end
|
302
|
+
end.join(joiner)
|
303
|
+
"(#{sql})"
|
304
|
+
end
|
305
|
+
|
306
|
+
def escape(name)
|
307
|
+
"`#{name.to_s.gsub('`', '``')}`"
|
308
|
+
end
|
309
|
+
|
310
|
+
def escape_function(name)
|
311
|
+
name.to_s.gsub(/[^a-z0-9\_]/i, '').upcase
|
312
|
+
end
|
313
|
+
|
314
|
+
def value_escape(value)
|
315
|
+
if value == true
|
316
|
+
'1'
|
317
|
+
elsif value == false
|
318
|
+
'0'
|
319
|
+
elsif value.nil?
|
320
|
+
'NULL'
|
321
|
+
elsif value.is_a?(Integer)
|
322
|
+
value.to_i
|
323
|
+
else
|
324
|
+
if value.to_s.length == 0
|
325
|
+
'NULL'
|
326
|
+
else
|
327
|
+
escaped_value = @escape_block ? @escape_block.call(value.to_s) : value.to_s
|
328
|
+
"'" + escaped_value + "'"
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
def with_table_and_column(input, &block)
|
334
|
+
if input.is_a?(Hash)
|
335
|
+
input.each { |table, column| block.call(table, column) }
|
336
|
+
else
|
337
|
+
block.call(@table_name, input.to_sym)
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def column_tuple(table, column)
|
342
|
+
[escape(table), escape(column)].join('.')
|
343
|
+
end
|
344
|
+
|
345
|
+
end
|
346
|
+
end
|
data/lib/sqb/version.rb
ADDED
data/lib/sqb.rb
ADDED
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sqb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Adam Cooke
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-02-26 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A friendly SQL builder for MySQL.
|
14
|
+
email:
|
15
|
+
- me@adamcooke.io
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/sqb.rb
|
21
|
+
- lib/sqb/error.rb
|
22
|
+
- lib/sqb/query.rb
|
23
|
+
- lib/sqb/version.rb
|
24
|
+
homepage: https://github.com/adamcooke/sqb
|
25
|
+
licenses:
|
26
|
+
- MIT
|
27
|
+
metadata: {}
|
28
|
+
post_install_message:
|
29
|
+
rdoc_options: []
|
30
|
+
require_paths:
|
31
|
+
- lib
|
32
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '0'
|
37
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
requirements: []
|
43
|
+
rubyforge_project:
|
44
|
+
rubygems_version: 2.7.4
|
45
|
+
signing_key:
|
46
|
+
specification_version: 4
|
47
|
+
summary: This gem provides a friendly DSL for constructing MySQL queries.
|
48
|
+
test_files: []
|