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 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: []