fluent-query-sql 0.9.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.
- data/.document +5 -0
- data/Gemfile +22 -0
- data/Gemfile.lock +26 -0
- data/LICENSE.txt +20 -0
- data/README.md +33 -0
- data/Rakefile +31 -0
- data/VERSION +1 -0
- data/fluent-query-sql.gemspec +74 -0
- data/lib/fluent-query/drivers/shared/tokens/sql.rb +128 -0
- data/lib/fluent-query/drivers/shared/tokens/sql/delete.rb +58 -0
- data/lib/fluent-query/drivers/shared/tokens/sql/from.rb +103 -0
- data/lib/fluent-query/drivers/shared/tokens/sql/groupby.rb +117 -0
- data/lib/fluent-query/drivers/shared/tokens/sql/having.rb +100 -0
- data/lib/fluent-query/drivers/shared/tokens/sql/insert.rb +60 -0
- data/lib/fluent-query/drivers/shared/tokens/sql/join.rb +98 -0
- data/lib/fluent-query/drivers/shared/tokens/sql/orderby.rb +148 -0
- data/lib/fluent-query/drivers/shared/tokens/sql/select.rb +147 -0
- data/lib/fluent-query/drivers/shared/tokens/sql/set.rb +94 -0
- data/lib/fluent-query/drivers/shared/tokens/sql/truncate.rb +59 -0
- data/lib/fluent-query/drivers/shared/tokens/sql/union.rb +65 -0
- data/lib/fluent-query/drivers/shared/tokens/sql/update.rb +58 -0
- data/lib/fluent-query/drivers/shared/tokens/sql/where.rb +100 -0
- data/lib/fluent-query/drivers/sql.rb +559 -0
- data/lib/fluent-query/queries/sql.rb +54 -0
- metadata +135 -0
@@ -0,0 +1,559 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "abstract"
|
3
|
+
require "hash-utils/module" # >= 0.13.0
|
4
|
+
require "hash-utils/string" # >= 0.15.0
|
5
|
+
require "hash-utils/array" # >= 0.17.0
|
6
|
+
|
7
|
+
require "fluent-query/driver"
|
8
|
+
require "fluent-query/queries/sql"
|
9
|
+
require "fluent-query/drivers/shared/tokens/sql"
|
10
|
+
|
11
|
+
module FluentQuery
|
12
|
+
module Drivers
|
13
|
+
|
14
|
+
##
|
15
|
+
# Represents abstract SQL driver.
|
16
|
+
#
|
17
|
+
|
18
|
+
class SQL < FluentQuery::Driver
|
19
|
+
|
20
|
+
##
|
21
|
+
# Contains relevant methods index for this driver.
|
22
|
+
#
|
23
|
+
|
24
|
+
RELEVANT = [:select, :insert, :update, :delete, :truncate, :set, :begin, :commit, :union]
|
25
|
+
|
26
|
+
##
|
27
|
+
# Contains ordering for typicall queries.
|
28
|
+
#
|
29
|
+
|
30
|
+
ORDERING = {
|
31
|
+
:select => [
|
32
|
+
:select, :from, :join, :groupBy, :having, :where, :orderBy, :limit, :offset
|
33
|
+
],
|
34
|
+
:insert => [
|
35
|
+
:insert, :values
|
36
|
+
],
|
37
|
+
:update => [
|
38
|
+
:update, :set, :where
|
39
|
+
],
|
40
|
+
:delete => [
|
41
|
+
:delete, :where
|
42
|
+
],
|
43
|
+
:truncate => [
|
44
|
+
:truncate, :table, :cascade
|
45
|
+
],
|
46
|
+
:set => [
|
47
|
+
:set
|
48
|
+
],
|
49
|
+
:union => [
|
50
|
+
:union
|
51
|
+
]
|
52
|
+
}
|
53
|
+
|
54
|
+
##
|
55
|
+
# Contains operators list.
|
56
|
+
#
|
57
|
+
# Operators are defined as tokens whose multiple parameters in Array
|
58
|
+
# are appropriate to join by itself.
|
59
|
+
#
|
60
|
+
|
61
|
+
OPERATORS = {
|
62
|
+
:and => "AND",
|
63
|
+
:or => "OR"
|
64
|
+
}
|
65
|
+
|
66
|
+
##
|
67
|
+
# Indicates, appropriate token should be present by one real token, but more input tokens.
|
68
|
+
#
|
69
|
+
|
70
|
+
AGREGATE = [:where, :orderBy, :select, :groupBy, :having]
|
71
|
+
|
72
|
+
##
|
73
|
+
# Indicates token aliases.
|
74
|
+
#
|
75
|
+
|
76
|
+
ALIASES = {
|
77
|
+
:leftJoin => :join,
|
78
|
+
:rightJoin => :join,
|
79
|
+
:fullJoin => :join
|
80
|
+
}
|
81
|
+
|
82
|
+
##
|
83
|
+
# Contains relevant methods index for this driver.
|
84
|
+
#
|
85
|
+
|
86
|
+
@relevant
|
87
|
+
|
88
|
+
##
|
89
|
+
# Contains ordering for typicall queries.
|
90
|
+
#
|
91
|
+
|
92
|
+
@ordering
|
93
|
+
|
94
|
+
##
|
95
|
+
# Contains operators list.
|
96
|
+
#
|
97
|
+
# Operators are defined as tokens whose multiple parameters in Array
|
98
|
+
# are appropriate to join by itself.
|
99
|
+
#
|
100
|
+
|
101
|
+
@operators
|
102
|
+
|
103
|
+
##
|
104
|
+
# Indicates, appropriate token should be present by one real token, but more input tokens.
|
105
|
+
#
|
106
|
+
|
107
|
+
@agregate
|
108
|
+
|
109
|
+
##
|
110
|
+
# Indicates token aliases.
|
111
|
+
#
|
112
|
+
|
113
|
+
@aliases
|
114
|
+
|
115
|
+
##
|
116
|
+
# Holds native connection settings.
|
117
|
+
#
|
118
|
+
|
119
|
+
@_nconnection_settings
|
120
|
+
|
121
|
+
##
|
122
|
+
# Holds native connection.
|
123
|
+
# (internal cache)
|
124
|
+
#
|
125
|
+
|
126
|
+
@_nconnection
|
127
|
+
|
128
|
+
##
|
129
|
+
# Indicates tokens which has been already required.
|
130
|
+
#
|
131
|
+
|
132
|
+
@_tokens_required
|
133
|
+
|
134
|
+
##
|
135
|
+
# Holds internal token struct.
|
136
|
+
#
|
137
|
+
|
138
|
+
TOKEN = Struct::new(:name, :subtokens)
|
139
|
+
|
140
|
+
##
|
141
|
+
# Initializes driver.
|
142
|
+
#
|
143
|
+
|
144
|
+
public
|
145
|
+
def initialize(connection)
|
146
|
+
if self.instance_of? SQL
|
147
|
+
not_implemented
|
148
|
+
end
|
149
|
+
|
150
|
+
super(connection)
|
151
|
+
|
152
|
+
@relevant = SQL::RELEVANT
|
153
|
+
@ordering = SQL::ORDERING
|
154
|
+
@operators = SQL::OPERATORS
|
155
|
+
@aliases = SQL::ALIASES
|
156
|
+
|
157
|
+
@agregate = { }
|
158
|
+
SQL::AGREGATE.each do |i|
|
159
|
+
@agregate[i] = true
|
160
|
+
end
|
161
|
+
|
162
|
+
@_tokens_required = { }
|
163
|
+
end
|
164
|
+
|
165
|
+
####
|
166
|
+
|
167
|
+
##
|
168
|
+
# Indicates, method is relevant for the driver.
|
169
|
+
#
|
170
|
+
|
171
|
+
public
|
172
|
+
def relevant_method?(name)
|
173
|
+
@relevant.include? name
|
174
|
+
end
|
175
|
+
|
176
|
+
##
|
177
|
+
# Indicates token is known.
|
178
|
+
#
|
179
|
+
|
180
|
+
public
|
181
|
+
def known_token?(group, token_name, cache)
|
182
|
+
|
183
|
+
# Checks
|
184
|
+
if @ordering.has_key? group
|
185
|
+
# Creates index
|
186
|
+
if not cache.has_key? group
|
187
|
+
@ordering[group].each do |item|
|
188
|
+
cache[group][item] = true
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
result = cache[group].has_key? token_name.to_sym
|
193
|
+
else
|
194
|
+
result = false
|
195
|
+
end
|
196
|
+
|
197
|
+
return result
|
198
|
+
|
199
|
+
end
|
200
|
+
|
201
|
+
##
|
202
|
+
# Builds given query.
|
203
|
+
#
|
204
|
+
|
205
|
+
public
|
206
|
+
def build_query(query, mode = :build)
|
207
|
+
self._build_query(query, FluentQuery::Drivers::Shared::Tokens::SQLToken, mode)
|
208
|
+
end
|
209
|
+
|
210
|
+
##
|
211
|
+
# Indicates, token is operator.
|
212
|
+
#
|
213
|
+
|
214
|
+
public
|
215
|
+
def operator_token?(token_name)
|
216
|
+
@operators.has_key? token_name
|
217
|
+
end
|
218
|
+
|
219
|
+
##
|
220
|
+
# Returns operator string according to operator symbol.
|
221
|
+
#
|
222
|
+
|
223
|
+
public
|
224
|
+
def quote_operator(operator)
|
225
|
+
@operators[operator]
|
226
|
+
end
|
227
|
+
|
228
|
+
##
|
229
|
+
# Returns correct equality operator for given datatype.
|
230
|
+
#
|
231
|
+
# Must handle three modes:
|
232
|
+
# * "assigning" which classicaly keeps for example the '=' operator,
|
233
|
+
# * "comparing" which sets for example 'IS' operator for booleans,
|
234
|
+
#
|
235
|
+
|
236
|
+
public
|
237
|
+
def quote_equality(datatype, mode = :comparing)
|
238
|
+
if not datatype.kind_of? Class
|
239
|
+
datatype = datatype.class
|
240
|
+
end
|
241
|
+
|
242
|
+
if mode == :comparing
|
243
|
+
if (datatype == TrueClass) or (datatype == FalseClass) or (datatype == NilClass)
|
244
|
+
result = "IS"
|
245
|
+
elsif datatype == Array
|
246
|
+
result = "IN"
|
247
|
+
else
|
248
|
+
result = "="
|
249
|
+
end
|
250
|
+
else
|
251
|
+
result = "="
|
252
|
+
end
|
253
|
+
|
254
|
+
return result
|
255
|
+
end
|
256
|
+
|
257
|
+
##
|
258
|
+
# Indicates which query subclass to use.
|
259
|
+
#
|
260
|
+
|
261
|
+
public
|
262
|
+
def query_class
|
263
|
+
FluentQuery::Queries::SQL
|
264
|
+
end
|
265
|
+
|
266
|
+
##
|
267
|
+
# Builds given query.
|
268
|
+
#
|
269
|
+
|
270
|
+
protected
|
271
|
+
def _build_query(query, inittoken_class, mode = :build)
|
272
|
+
|
273
|
+
stack = query.stack
|
274
|
+
token_struct = self.class::TOKEN
|
275
|
+
tokens = [ ]
|
276
|
+
|
277
|
+
last_known = nil
|
278
|
+
result = ""
|
279
|
+
|
280
|
+
|
281
|
+
# Gets type of the query (select/update/insert/delete)
|
282
|
+
|
283
|
+
type = query.type
|
284
|
+
|
285
|
+
# Sorts according to known token type
|
286
|
+
#
|
287
|
+
# Unknown tokens are joined to the same subarray as preceded
|
288
|
+
# known tokens.
|
289
|
+
#
|
290
|
+
|
291
|
+
stack.each do |item|
|
292
|
+
name = item.name
|
293
|
+
|
294
|
+
# If token is alias, renames it
|
295
|
+
|
296
|
+
_alias = @aliases[name]
|
297
|
+
|
298
|
+
if not _alias.nil?
|
299
|
+
#_alias = _alias
|
300
|
+
|
301
|
+
item.alias = _alias
|
302
|
+
name = _alias
|
303
|
+
end
|
304
|
+
|
305
|
+
##
|
306
|
+
|
307
|
+
if self.known_token? type, name
|
308
|
+
|
309
|
+
if not last_known.nil?
|
310
|
+
last_name = last_known.first.name
|
311
|
+
tokens.push(token_struct::new(last_name, last_known))
|
312
|
+
end
|
313
|
+
|
314
|
+
last_known = [item]
|
315
|
+
|
316
|
+
elsif not last_known.array?
|
317
|
+
tokens.push(token_struct::new(:__init__, [item]))
|
318
|
+
|
319
|
+
else
|
320
|
+
last_known << item
|
321
|
+
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
if last_known
|
326
|
+
last_name = last_known.first.name
|
327
|
+
tokens.push(token_struct::new(last_name, last_known))
|
328
|
+
end
|
329
|
+
|
330
|
+
# Generates native tokens
|
331
|
+
|
332
|
+
native_tokens = [ ]
|
333
|
+
init_tokens = nil
|
334
|
+
agregates = Hash::new { |hash, key| hash[key] = [ ] }
|
335
|
+
|
336
|
+
tokens.each do |data|
|
337
|
+
name = data.name
|
338
|
+
subtokens = data.subtokens
|
339
|
+
|
340
|
+
if (name == :__init__) and (not subtokens.nil?)
|
341
|
+
init_tokens = [inittoken_class::new(self, query, subtokens)]
|
342
|
+
|
343
|
+
elsif @agregate.has_key? name
|
344
|
+
agregates[name] << data
|
345
|
+
|
346
|
+
elsif self.known_token? type, name
|
347
|
+
token_data = self._generate_token(name, query, subtokens)
|
348
|
+
native_tokens << token_data
|
349
|
+
|
350
|
+
end
|
351
|
+
|
352
|
+
end
|
353
|
+
|
354
|
+
# Process agregate tokens
|
355
|
+
|
356
|
+
agregates.each_value do |tokens|
|
357
|
+
first = tokens.shift
|
358
|
+
|
359
|
+
tokens.each do |token|
|
360
|
+
first.subtokens += token.subtokens
|
361
|
+
end
|
362
|
+
|
363
|
+
token_data = self._generate_token(first.name, query, first.subtokens)
|
364
|
+
native_tokens << token_data
|
365
|
+
end
|
366
|
+
|
367
|
+
# Renders in ordered way
|
368
|
+
if not init_tokens.nil?
|
369
|
+
init_tokens.each do |token|
|
370
|
+
result << token.render!(mode) << " "
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
if @ordering.has_key? type
|
375
|
+
@ordering[type].each do |item|
|
376
|
+
native_tokens.each do |data|
|
377
|
+
if data.name == item
|
378
|
+
data.subtokens.each do |token|
|
379
|
+
result << token.render!(mode) << " "
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
result.strip!
|
387
|
+
return result
|
388
|
+
end
|
389
|
+
|
390
|
+
##
|
391
|
+
# Generates token from token data.
|
392
|
+
#
|
393
|
+
|
394
|
+
protected
|
395
|
+
def _generate_token(name, query, subtokens)
|
396
|
+
classobject = self._require_token(name)
|
397
|
+
tokens = [classobject::new(self, query, subtokens)]
|
398
|
+
|
399
|
+
###
|
400
|
+
|
401
|
+
return self.class::TOKEN::new(name, tokens)
|
402
|
+
end
|
403
|
+
|
404
|
+
##
|
405
|
+
# Requires token class.
|
406
|
+
#
|
407
|
+
|
408
|
+
protected
|
409
|
+
def _require_token(name)
|
410
|
+
|
411
|
+
if not @_tokens_required.has_key? name
|
412
|
+
begin
|
413
|
+
module_name = name.to_s.ucfirst
|
414
|
+
classname = "FluentQuery::Drivers::Shared::Tokens::SQL::" << module_name
|
415
|
+
classfile = classname.downcase
|
416
|
+
classfile.strtr!("::" => "/", "fluentquery" => "fluent-query")
|
417
|
+
|
418
|
+
Kernel.require(classfile)
|
419
|
+
classobject = FluentQuery::Drivers::Shared::Tokens::SQL.get_module(module_name)
|
420
|
+
rescue NameError, LoadError
|
421
|
+
classobject = FluentQuery::Drivers::Shared::Tokens::SQLToken
|
422
|
+
end
|
423
|
+
|
424
|
+
@_tokens_required[name] = classobject
|
425
|
+
end
|
426
|
+
|
427
|
+
return @_tokens_required[name]
|
428
|
+
end
|
429
|
+
|
430
|
+
|
431
|
+
|
432
|
+
|
433
|
+
##### QUOTING
|
434
|
+
|
435
|
+
##
|
436
|
+
# Quotes string.
|
437
|
+
#
|
438
|
+
|
439
|
+
public
|
440
|
+
def quote_string(string)
|
441
|
+
string = string.gsub("'", "''")
|
442
|
+
string.gsub!("\\", "\\\\\\\\")
|
443
|
+
|
444
|
+
return "'" << string << "'"
|
445
|
+
end
|
446
|
+
|
447
|
+
##
|
448
|
+
# Quotes integer.
|
449
|
+
#
|
450
|
+
|
451
|
+
public
|
452
|
+
def quote_integer(integer)
|
453
|
+
integer.to_s
|
454
|
+
end
|
455
|
+
|
456
|
+
##
|
457
|
+
# Quotes float.
|
458
|
+
#
|
459
|
+
|
460
|
+
public
|
461
|
+
def quote_float(float)
|
462
|
+
float.to_s
|
463
|
+
end
|
464
|
+
|
465
|
+
##
|
466
|
+
# Quotes field by field quoting.
|
467
|
+
#
|
468
|
+
|
469
|
+
public
|
470
|
+
def quote_identifier(field)
|
471
|
+
'"' << field.to_s.gsub(".", '"."') << '"'
|
472
|
+
end
|
473
|
+
|
474
|
+
##
|
475
|
+
# Creates system-dependent NULL.
|
476
|
+
#
|
477
|
+
|
478
|
+
public
|
479
|
+
def null
|
480
|
+
"NULL"
|
481
|
+
end
|
482
|
+
|
483
|
+
##
|
484
|
+
# Quotes system-dependent boolean value.
|
485
|
+
#
|
486
|
+
|
487
|
+
public
|
488
|
+
def quote_boolean(boolean)
|
489
|
+
boolean ? 1 : 0
|
490
|
+
end
|
491
|
+
|
492
|
+
##
|
493
|
+
# Quotes system-dependent date value.
|
494
|
+
#
|
495
|
+
|
496
|
+
public
|
497
|
+
def quote_date_time(date)
|
498
|
+
self.quote_string(date.to_s)
|
499
|
+
end
|
500
|
+
|
501
|
+
##
|
502
|
+
# Quotes system-dependent subquery.
|
503
|
+
#
|
504
|
+
|
505
|
+
public
|
506
|
+
def quote_subquery(subquery)
|
507
|
+
"(" << subquery << ")"
|
508
|
+
end
|
509
|
+
|
510
|
+
|
511
|
+
##### EXECUTING
|
512
|
+
|
513
|
+
##
|
514
|
+
# Opens the connection.
|
515
|
+
#
|
516
|
+
# It's lazy, so it will open connection before first request through
|
517
|
+
# {@link native_connection()} method.
|
518
|
+
#
|
519
|
+
|
520
|
+
public
|
521
|
+
def open_connection(settings)
|
522
|
+
not_implemented
|
523
|
+
end
|
524
|
+
|
525
|
+
##
|
526
|
+
# Executes query conditionally.
|
527
|
+
#
|
528
|
+
# If query isn't suitable for executing, returns it. In otherwise
|
529
|
+
# returns result or number of changed rows.
|
530
|
+
#
|
531
|
+
|
532
|
+
public
|
533
|
+
def execute_conditionally(query, sym, *args, &block)
|
534
|
+
case query.type
|
535
|
+
when :insert
|
536
|
+
if (args.first.symbol?) and (args.second.hash?)
|
537
|
+
result = query.do!
|
538
|
+
end
|
539
|
+
when :begin
|
540
|
+
if args.empty?
|
541
|
+
result = query.execute!
|
542
|
+
end
|
543
|
+
when :truncate
|
544
|
+
if args.first.symbol?
|
545
|
+
result = query.execute!
|
546
|
+
end
|
547
|
+
when :commit, :rollback
|
548
|
+
result = query.execute!
|
549
|
+
else
|
550
|
+
result = nil
|
551
|
+
end
|
552
|
+
|
553
|
+
return result
|
554
|
+
end
|
555
|
+
|
556
|
+
end
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|