sql-maker 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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>