sqb 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,4 @@
1
+ module SQB
2
+ class Error < StandardError
3
+ end
4
+ end
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
@@ -0,0 +1,3 @@
1
+ module SQB
2
+ VERSION = '1.0.0'
3
+ end
data/lib/sqb.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'sqb/query'
2
+ require 'sqb/version'
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: []