sql-maker 0.0.1

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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/CHANGELOG.md +3 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +23 -0
  7. data/Rakefile +14 -0
  8. data/lib/sql-maker.rb +5 -0
  9. data/lib/sql/maker.rb +676 -0
  10. data/lib/sql/maker/condition.rb +378 -0
  11. data/lib/sql/maker/error.rb +3 -0
  12. data/lib/sql/maker/helper.rb +22 -0
  13. data/lib/sql/maker/quoting.rb +138 -0
  14. data/lib/sql/maker/select.rb +544 -0
  15. data/lib/sql/maker/select/oracle.rb +30 -0
  16. data/lib/sql/maker/select_set.rb +194 -0
  17. data/lib/sql/maker/util.rb +54 -0
  18. data/lib/sql/query_maker.rb +429 -0
  19. data/scripts/perl2ruby.rb +34 -0
  20. data/spec/maker/bind_param_spec.rb +62 -0
  21. data/spec/maker/condition/add_raw_spec.rb +29 -0
  22. data/spec/maker/condition/compose_empty_spec.rb +72 -0
  23. data/spec/maker/condition/empty_values_spec.rb +25 -0
  24. data/spec/maker/condition/make_term_spec.rb +38 -0
  25. data/spec/maker/condition/where_spec.rb +35 -0
  26. data/spec/maker/delete_spec.rb +116 -0
  27. data/spec/maker/insert_empty_spec.rb +23 -0
  28. data/spec/maker/insert_spec.rb +61 -0
  29. data/spec/maker/new_line_spec.rb +9 -0
  30. data/spec/maker/select/oracle/oracle_spec.rb +16 -0
  31. data/spec/maker/select/pod_select_spec.rb +34 -0
  32. data/spec/maker/select/statement_spec.rb +805 -0
  33. data/spec/maker/select_set_spec.rb +294 -0
  34. data/spec/maker/select_spec.rb +470 -0
  35. data/spec/maker/simple_spec.rb +54 -0
  36. data/spec/maker/strict_spec.rb +45 -0
  37. data/spec/maker/subquery_spec.rb +77 -0
  38. data/spec/maker/update_spec.rb +138 -0
  39. data/spec/maker/util_spec.rb +6 -0
  40. data/spec/maker/where_spec.rb +28 -0
  41. data/spec/query_maker/and_using_hash_spec.rb +13 -0
  42. data/spec/query_maker/cheatsheet_spec.rb +32 -0
  43. data/spec/query_maker/column_bind_spec.rb +55 -0
  44. data/spec/query_maker/refs_in_bind_spec.rb +19 -0
  45. data/spec/spec_helper.rb +15 -0
  46. data/sql-maker.gemspec +25 -0
  47. metadata +185 -0
@@ -0,0 +1,544 @@
1
+ require 'sql/maker/condition'
2
+ require 'sql/maker/util'
3
+
4
+ class SQL::Maker::Select
5
+ include SQL::Maker::Util
6
+
7
+ attr_reader :quote_char, :name_sep, :new_line, :strict, :auto_bind
8
+ attr_accessor :select, :select_map, :select_map_reverse, :from, :joins,
9
+ :index_hint, :group_by, :order_by, :where, :having, :for_update, :subqueries
10
+
11
+ def initialize(args = {})
12
+ @select = args[:select] || []
13
+ @distinct = args[:distinct] || false
14
+ @select_map = args[:select_map] || {}
15
+ @select_map_reverse = args[:select_map_reverse] || {}
16
+ @from = args[:from] || []
17
+ @joins = args[:joins] || []
18
+ @index_hint = args[:index_hint] || {}
19
+ @group_by = args[:group_by] || []
20
+ @order_by = args[:order_by] || []
21
+ @prefix = args[:prefix] || 'SELECT '
22
+ @new_line = args[:new_line] || "\n"
23
+ @strict = args[:strict] || false
24
+ @auto_bind = args[:auto_bind] || false
25
+ @where = args[:where]
26
+ @having = args[:having]
27
+ @limit = args[:limit]
28
+ @offset = args[:offset]
29
+ @for_update = args[:for_update]
30
+ @quote_char = args[:quote_char]
31
+ @name_sep = args[:name_sep]
32
+ @subqueries = []
33
+ end
34
+
35
+ def distinct(distinct = nil)
36
+ if distinct
37
+ @distinct = distinct
38
+ self # method chain
39
+ else
40
+ @distinct
41
+ end
42
+ end
43
+
44
+ def prefix(prefix = nil)
45
+ if prefix
46
+ @prefix = prefix
47
+ self # method chain
48
+ else
49
+ @prefix
50
+ end
51
+ end
52
+
53
+ def offset(offset = nil)
54
+ if offset
55
+ @offset = offset
56
+ self # method chain
57
+ else
58
+ @offset
59
+ end
60
+ end
61
+
62
+ def limit(limit = nil)
63
+ if limit
64
+ @limit = limit
65
+ self # method chain
66
+ else
67
+ @limit
68
+ end
69
+ end
70
+
71
+ def new_condition
72
+ SQL::Maker::Condition.new(
73
+ :quote_char => self.quote_char,
74
+ :name_sep => self.name_sep,
75
+ :strict => self.strict,
76
+ )
77
+ end
78
+
79
+ def bind
80
+ bind = []
81
+ bind += self.subqueries if self.subqueries
82
+ bind += self.where.bind if self.where
83
+ bind += self.having.bind if self.having
84
+ bind
85
+ end
86
+
87
+ def add_select(*args)
88
+ term, col = parse_args(*args)
89
+ term = term.to_s if term.is_a?(Symbol)
90
+ col ||= term
91
+ self.select += array_wrap(term)
92
+ self.select_map[term] = col
93
+ self.select_map_reverse[col] = term
94
+ self # method chain
95
+ end
96
+
97
+ def add_from(*args)
98
+ table, as = parse_args(*args)
99
+ if table.respond_to?(:as_sql)
100
+ self.subqueries += table.bind
101
+ self.from += [[table, as]]
102
+ else
103
+ table = table.to_s
104
+ self.from += [[table, as]]
105
+ end
106
+ self
107
+ end
108
+
109
+ def add_join(*args)
110
+ # :user => { :type => 'inner', :table => 'config', :condition => {'user.user_id' => 'config.user_id'} }
111
+ # [ subquery, 'bar' ] => { :type => 'inner', :table => 'config', :condition => {'user.user_id' => 'config.user_id'} }
112
+ table, joins = parse_args(*args)
113
+ table, as = parse_args(*table)
114
+
115
+ if table.respond_to?(:as_sql)
116
+ self.subqueries += table.bind
117
+ table = '(' + table.as_sql + ')'
118
+ else
119
+ table = table.to_s
120
+ end
121
+
122
+ self.joins += [{
123
+ :table => [ table, as ],
124
+ :joins => joins
125
+ }]
126
+ self
127
+ end
128
+
129
+ def add_index_hint(*args)
130
+ table, hint = parse_args(*args)
131
+ table = table.to_s
132
+ if hint.is_a?(Hash)
133
+ # { :type => '...', :list => ['foo'] }
134
+ type = hint[:type] || 'USE'
135
+ list = array_wrap(hint[:list])
136
+ else
137
+ # ['foo, 'bar'] or just 'foo'
138
+ type = 'USE'
139
+ list = array_wrap(hint)
140
+ end
141
+
142
+ self.index_hint[table] = {
143
+ :type => type,
144
+ :list => list,
145
+ }
146
+
147
+ return self
148
+ end
149
+
150
+ def _quote(label)
151
+ SQL::Maker::Util.quote_identifier(label, self.quote_char, self.name_sep)
152
+ end
153
+
154
+ def as_sql
155
+ sql = ''
156
+ new_line = self.new_line
157
+
158
+ unless self.select.empty?
159
+ sql += self.prefix
160
+ sql += 'DISTINCT ' if self.distinct
161
+ sql += self.select.map {|col|
162
+ as = self.select_map[col]
163
+ col = col.respond_to?(:as_sql) ? col.as_sql : self._quote(col)
164
+ next col if as.nil?
165
+ as = as.respond_to?(:as_sql) ? as.as_sql : self._quote(as)
166
+ if as && col =~ /(?:^|\.)#{Regexp.escape(as)}$/
167
+ col
168
+ else
169
+ col + ' AS ' + as
170
+ end
171
+ }.join(', ') + new_line
172
+ end
173
+
174
+ sql += 'FROM '
175
+
176
+ ## Add any explicit JOIN statements before the non-joined tables.
177
+ unless self.joins.empty?
178
+ initial_table_written = 0
179
+ self.joins.each do |j|
180
+ table = j[:table]
181
+ join = j[:joins]
182
+ table = self._add_index_hint(table); ## index hint handling
183
+ sql += table if initial_table_written == 0
184
+ initial_table_written += 1
185
+ sql += ' ' + join[:type].upcase if join[:type]
186
+ sql += ' JOIN ' + self._quote(join[:table])
187
+ sql += ' ' + self._quote(join[:alias]) if join[:alias]
188
+
189
+ if condition = join[:condition]
190
+ if condition.is_a?(Array)
191
+ sql += ' USING (' + condition.map {|e| self._quote(e) }.join(', ') + ')'
192
+ elsif condition.is_a?(Hash)
193
+ conds = []
194
+ condition.keys.each do |key|
195
+ conds += [self._quote(key) + ' = ' + self._quote(condition[key])]
196
+ end
197
+ sql += ' ON ' + conds.join(' AND ')
198
+ else
199
+ sql += ' ON ' + condition
200
+ end
201
+ end
202
+ end
203
+ sql += ', ' unless self.from.empty?
204
+ end
205
+
206
+ unless self.from.empty?
207
+ sql += self.from.map {|e| self._add_index_hint(e[0], e[1]) }.join(', ')
208
+ end
209
+
210
+ sql += new_line
211
+ sql += self.as_sql_where if self.where
212
+
213
+ sql += self.as_sql_group_by if self.group_by
214
+ sql += self.as_sql_having if self.having
215
+ sql += self.as_sql_order_by if self.order_by
216
+
217
+ sql += self.as_sql_limit if self.limit
218
+
219
+ sql += self.as_sql_for_update
220
+ sql.gsub!(/#{new_line}+$/, '')
221
+
222
+ @auto_bind ? bind_param(sql, self.bind) : sql
223
+ end
224
+
225
+ def as_sql_limit
226
+ return '' unless n = self.limit
227
+ croak("Non-numerics in limit clause (n)") if n =~ /\D/
228
+ return sprintf "LIMIT %d%s" + self.new_line, n,
229
+ (self.offset ? " OFFSET " + self.offset.to_i.to_s : "")
230
+ end
231
+
232
+ def add_order_by(*args)
233
+ col, type = parse_args(*args)
234
+ self.order_by += [[col, type]]
235
+ return self
236
+ end
237
+
238
+ def as_sql_order_by
239
+ attrs = self.order_by
240
+ return '' if attrs.empty?
241
+
242
+ return 'ORDER BY ' + attrs.map {|e|
243
+ col, type = e
244
+ if col.respond_to?(:as_sql)
245
+ col.as_sql
246
+ else
247
+ type ? self._quote(col) + " #{type}" : self._quote(col)
248
+ end
249
+ }.join(', ') + self.new_line
250
+ end
251
+
252
+ def add_group_by(*args)
253
+ group, order = parse_args(*args)
254
+ self.group_by +=
255
+ if group.respond_to?(:as_sql)
256
+ [group.as_sql]
257
+ else
258
+ order ? [self._quote(group) + " #{order}"] : [self._quote(group)]
259
+ end
260
+ return self
261
+ end
262
+
263
+ def as_sql_group_by
264
+ elems = self.group_by
265
+ return '' if elems.empty?
266
+
267
+ return 'GROUP BY ' + elems.join(', ') + self.new_line
268
+ end
269
+
270
+ def set_where(where)
271
+ self.where = where
272
+ return self
273
+ end
274
+
275
+ def add_where(*args)
276
+ self.where ||= self.new_condition()
277
+ self.where.add(*args)
278
+ return self
279
+ end
280
+
281
+ def add_where_raw(*args)
282
+ self.where ||= self.new_condition()
283
+ self.where.add_raw(*args)
284
+ return self
285
+ end
286
+
287
+ def as_sql_where
288
+ where = self.where.as_sql()
289
+ where and !where.empty? ? "WHERE #{where}" + self.new_line : ''
290
+ end
291
+
292
+ def as_sql_having
293
+ if self.having
294
+ 'HAVING ' + self.having.as_sql + self.new_line
295
+ else
296
+ ''
297
+ end
298
+ end
299
+
300
+ def add_having(*args)
301
+ col, val = parse_args(*args)
302
+ col = col.to_s
303
+ if orig = self.select_map_reverse[col]
304
+ col = orig
305
+ end
306
+
307
+ self.having ||= self.new_condition()
308
+ self.having.add(col, val)
309
+ return self
310
+ end
311
+
312
+ def as_sql_for_update
313
+ self.for_update ? ' FOR UPDATE' : ''
314
+ end
315
+
316
+ def _add_index_hint(*args)
317
+ table, as = parse_args(*args)
318
+ tbl_name =
319
+ if table.respond_to?(:as_sql)
320
+ '(' + table.as_sql + ')'
321
+ else
322
+ self._quote(table)
323
+ end
324
+ quoted = as ? tbl_name + ' ' + self._quote(as) : tbl_name
325
+ hint = self.index_hint[table]
326
+ return quoted unless hint && hint.is_a?(Hash)
327
+ if hint[:list]&& !hint[:list].empty?
328
+ return quoted + ' ' + (hint[:type].upcase || 'USE') + ' INDEX (' +
329
+ hint[:list].map {|e| self._quote(e) }.join(',') + ')'
330
+ end
331
+ return quoted
332
+ end
333
+ end
334
+
335
+ __END__
336
+
337
+ =head1 NAME
338
+
339
+ SQL::Maker::Select - dynamic SQL generator
340
+
341
+ =head1 SYNOPSIS
342
+
343
+ sql = SQL::Maker::Select.new
344
+ .add_select('foo')
345
+ .add_select('bar')
346
+ .add_select('baz')
347
+ .add_from('table_name' => 't')
348
+ .as_sql
349
+ # => "SELECT foo, bar, baz FROM table_name t"
350
+
351
+ =head1 DESCRIPTION
352
+
353
+ =head1 METHODS
354
+
355
+ =over 4
356
+
357
+ =item C<< sql = stmt.as_sql(); >>
358
+
359
+ Render the SQL string.
360
+
361
+ =item C<< @bind = stmt.bind(); >>
362
+
363
+ Get the bind variables.
364
+
365
+ =item C<< stmt.add_select('*') >>
366
+
367
+ =item C<< stmt.add_select(:col => alias) >>
368
+
369
+ =item C<< stmt.add_select(\'COUNT(*)' => 'cnt') >>
370
+
371
+ Add a new select term. It's automatically quoted.
372
+
373
+ =item C<< stmt.add_from(table :Str | select :SQL::Maker::Select) : SQL::Maker::Select >>
374
+
375
+ Add a new FROM clause. You can specify the table name or an instance of L<SQL::Maker::Select> for a sub-query.
376
+
377
+ I<Return:> stmt itself.
378
+
379
+ =item C<< stmt.add_join(:user => {:type => 'inner', :table => 'config', :condition => 'user.user_id = config.user_id'}); >>
380
+
381
+ =item C<< stmt.add_join(:user => {:type => 'inner', :table => 'config', :condition => {'user.user_id' => 'config.user_id'}); >>
382
+
383
+ =item C<< stmt.add_join(:user => {:type => 'inner', :table => 'config', :condition => ['user_id']}); >>
384
+
385
+ Add a new JOIN clause. If you pass an arrayref for 'condition' then it uses 'USING'. If 'type' is omitted
386
+ it falls back to plain JOIN.
387
+
388
+ stmt = SQL::Maker::Select.new
389
+ stmt.add_join(
390
+ :user => {
391
+ :type => 'inner',
392
+ :table => 'config',
393
+ :condition => 'user.user_id = config.user_id',
394
+ }
395
+ )
396
+ stmt.as_sql
397
+ # => 'FROM user INNER JOIN config ON user.user_id = config.user_id'
398
+
399
+ stmt = SQL::Maker::Select.new(:quote_char => '`', :name_sep => '.')
400
+ stmt.add_join(
401
+ :user => {
402
+ :type => 'inner',
403
+ :table => 'config',
404
+ :condition => {'user.user_id' => 'config.user_id'},
405
+ }
406
+ )
407
+ stmt.as_sql
408
+ # => 'FROM `user` INNER JOIN `config` ON `user`.`user_id` = `config`.`user_id`'
409
+
410
+ stmt = SQL::Maker::Select.new
411
+ stmt.add_select('name')
412
+ stmt.add_join(
413
+ :user => {
414
+ :type => 'inner',
415
+ :table => 'config',
416
+ :condition => ['user_id'],
417
+ }
418
+ )
419
+ stmt.as_sql
420
+ # => 'SELECT name FROM user INNER JOIN config USING (user_id)'
421
+
422
+ subquery = SQL::Maker::Select.new
423
+ subquery.add_select('*')
424
+ subquery.add_from( 'foo' )
425
+ subquery.add_where( 'hoge' => 'fuga' )
426
+ stmt = SQL::Maker::Select.new
427
+ stmt.add_join(
428
+ [ subquery, 'bar' ] => {
429
+ :type => 'inner',
430
+ :table => 'baz',
431
+ :alias => 'b1',
432
+ :condition => 'bar.baz_id = b1.baz_id'
433
+ },
434
+ )
435
+ stmt.as_sql
436
+ # => "FROM (SELECT * FROM foo WHERE (hoge = ?)) bar INNER JOIN baz b1 ON bar.baz_id = b1.baz_id"
437
+
438
+ =item C<< stmt.add_index_hint(:foo => {:type => 'USE', :list => ['index_hint']}); >>
439
+
440
+ =item C<< stmt.add_index_hint(:foo => 'index_hint'); >>
441
+
442
+ =item C<< stmt.add_index_hint(:foo => ['index_hint']); >>
443
+
444
+ stmt = SQL::Maker::Select.new
445
+ stmt.add_select('name')
446
+ stmt.add_from('user')
447
+ stmt.add_index_hint(:user => {:type => 'USE', :list => ['index_hint']})
448
+ stmt.as_sql
449
+ # => "SELECT name FROM user USE INDEX (index_hint)"
450
+
451
+ =item C<< stmt.add_where('foo_id' => 'bar'); >>
452
+
453
+ Add a new WHERE clause.
454
+
455
+ stmt = SQL::Maker::Select.new.add_select('c')
456
+ .add_from('foo')
457
+ .add_where('name' => 'john')
458
+ .add_where('type' => {:IN => %w/1 2 3/})
459
+ .as_sql
460
+ # => "SELECT c FROM foo WHERE (name = ?) AND (type IN (?, ?, ?))"
461
+
462
+ =item C<< stmt.add_where_raw('id = ?', [1]) >>
463
+
464
+ Add a new WHERE clause from raw placeholder string and bind variables.
465
+
466
+ stmt = SQL::Maker::Select.new.add_select('c')
467
+ .add_from('foo')
468
+ .add_where_raw('EXISTS(SELECT * FROM bar WHERE name = ?)' => ['john'])
469
+ .add_where_raw('type IS NOT NULL')
470
+ .as_sql
471
+ # => "SELECT c FROM foo WHERE (EXISTS(SELECT * FROM bar WHERE name = ?)) AND (type IS NOT NULL)"
472
+
473
+
474
+ =item C<< stmt.set_where(condition) >>
475
+
476
+ Set the WHERE clause.
477
+
478
+ condition should be instance of L<SQL::Maker::Condition>.
479
+
480
+ cond1 = SQL::Maker::Condition.new.add("name" => "john")
481
+ cond2 = SQL::Maker::Condition.new.add("type" => {:IN => %w/1 2 3/})
482
+ stmt = SQL::Maker::Select.new.add_select('c')
483
+ .add_from('foo')
484
+ .set_where(cond1 & cond2)
485
+ .as_sql
486
+ # => "SELECT c FROM foo WHERE ((name = ?)) AND ((type IN (?, ?, ?)))"
487
+
488
+ =item C<< stmt.add_order_by('foo'); >>
489
+
490
+ =item C<< stmt.add_order_by({'foo' => 'DESC'}); >>
491
+
492
+ Add a new ORDER BY clause.
493
+
494
+ stmt = SQL::Maker::Select.new.add_select('c')
495
+ .add_from('foo')
496
+ .add_order_by('name' => 'DESC')
497
+ .add_order_by('id')
498
+ .as_sql
499
+ # => "SELECT c FROM foo ORDER BY name DESC, id"
500
+
501
+ =item C<< stmt.add_group_by('foo'); >>
502
+
503
+ Add a new GROUP BY clause.
504
+
505
+ stmt = SQL::Maker::Select.new.add_select('c')
506
+ .add_from('foo')
507
+ .add_group_by('id')
508
+ .as_sql
509
+ # => "SELECT c FROM foo GROUP BY id"
510
+
511
+ stmt = SQL::Maker::Select.new.add_select('c')
512
+ .add_from('foo')
513
+ .add_group_by('id' => 'DESC')
514
+ .as_sql
515
+ # => "SELECT c FROM foo GROUP BY id DESC"
516
+
517
+ =item C<< stmt.limit(30) >>
518
+
519
+ =item C<< stmt.offset(5) >>
520
+
521
+ Add LIMIT and OFFSET.
522
+
523
+ stmt = SQL::Maker::Select.new.add_select('c')
524
+ .add_from('foo')
525
+ .limit(30)
526
+ .offset(5)
527
+ .as_sql
528
+ # => "SELECT c FROM foo LIMIT 30 OFFSET 5"
529
+
530
+ =item C<< stmt.add_having(:cnt => 2) >>
531
+
532
+ Add a HAVING clause.
533
+
534
+ # stmt = SQL::Maker::Select.new.add_from('foo')
535
+ # .add_select(\'COUNT(*)' => 'cnt')
536
+ # .add_having(:cnt => 2)
537
+ # .as_sql
538
+ # # => "SELECT COUNT(*) AS cnt FROM foo HAVING (COUNT(*) = ?)"
539
+
540
+ =back
541
+
542
+ =head1 SEE ALSO
543
+
544
+ L<Data::ObjectDriver::SQL>