fluent-query 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.
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+ require "fluent-query/exception"
3
+
4
+ module FluentQuery
5
+ module Drivers
6
+ ##
7
+ # Represents general fluent exception.
8
+ #
9
+
10
+ class Exception < FluentQuery::Exception
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,98 @@
1
+ # encoding: utf-8
2
+ require "abstract"
3
+
4
+ module FluentQuery
5
+ module Drivers
6
+
7
+ ##
8
+ # Represents general driver result accessor.
9
+ #
10
+
11
+ class Result
12
+
13
+ ##
14
+ # Initializes result.
15
+ #
16
+
17
+ public
18
+ def initialize
19
+ if self.instance_of? FluentQuery::Drivers::Result
20
+ not_implemented
21
+ end
22
+ end
23
+
24
+ ##
25
+ # Returns all selected rows.
26
+ #
27
+
28
+ public
29
+ def all
30
+ not_implemented
31
+ end
32
+
33
+ ##
34
+ # Returns one row.
35
+ #
36
+
37
+ public
38
+ def one
39
+ not_implemented
40
+ end
41
+
42
+ ##
43
+ # Returns first value of first row.
44
+ #
45
+
46
+ public
47
+ def single
48
+ not_implemented
49
+ end
50
+
51
+ ##
52
+ # Returns row as hash.
53
+ #
54
+
55
+ public
56
+ def hash
57
+ not_implemented
58
+ end
59
+
60
+ ##
61
+ # Handles iterating.
62
+ #
63
+
64
+ public
65
+ def each
66
+ not_implemented
67
+ end
68
+
69
+
70
+ ##
71
+ # Repeats the query leaded to the result.
72
+ #
73
+
74
+ public
75
+ def repeat!
76
+ not_implemented
77
+ end
78
+
79
+ ##
80
+ # Returns rows count.
81
+ #
82
+
83
+ public
84
+ def count
85
+ not_implemented
86
+ end
87
+
88
+ ##
89
+ # Frees result resources.
90
+ #
91
+
92
+ public
93
+ def free!
94
+ not_implemented
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,12 @@
1
+ # encoding: utf-8
2
+
3
+ module FluentQuery
4
+
5
+ ##
6
+ # Represents general fluent exception.
7
+ #
8
+
9
+ class Exception < ::Exception
10
+ end
11
+
12
+ end
@@ -0,0 +1,169 @@
1
+ # encoding: utf-8
2
+ require "fluent-query/queries/processor"
3
+ require "abstract"
4
+
5
+ module FluentQuery
6
+ module Queries
7
+
8
+ ##
9
+ # Represents and abstract query.
10
+ #
11
+
12
+ class Abstract
13
+
14
+ ##
15
+ # Holds connection object.
16
+ #
17
+
18
+ @connection
19
+ attr_reader :connection
20
+
21
+ ##
22
+ # Holds appropriate query processor.
23
+ #
24
+
25
+ @processor
26
+
27
+ ##
28
+ # Constructor.
29
+ #
30
+
31
+ public
32
+ def initialize(connection)
33
+ @connection = connection
34
+ end
35
+
36
+ ##
37
+ # Returns processor.
38
+ #
39
+
40
+ public
41
+ def processor
42
+ if not @processor
43
+ driver = @connection.driver
44
+ @processor = FluentQuery::Queries::Processor::new(driver)
45
+ end
46
+
47
+ return @processor
48
+ end
49
+
50
+ ##
51
+ # Builds prepared query string to final form.
52
+ #
53
+
54
+ public
55
+ def build(*args)
56
+ not_implemented
57
+ end
58
+
59
+ alias :"build!" :build
60
+
61
+ ##
62
+ # Executes query and returns result object.
63
+ #
64
+
65
+ public
66
+ def execute(*args)
67
+ @connection.execute(self.build(*args))
68
+ end
69
+
70
+ alias :"execute!" :execute
71
+
72
+ ##
73
+ # Executes query and returns count of changed/inserted rows.
74
+ #
75
+
76
+ public
77
+ def do(*args)
78
+ @connection.do(self.build(*args))
79
+ end
80
+
81
+ alias :"do!" :do
82
+
83
+
84
+ ##
85
+ # Returns all selected rows.
86
+ #
87
+
88
+ public
89
+ def all(*args)
90
+ self.execute(*args).all
91
+ end
92
+
93
+
94
+ ##
95
+ # Returns one row.
96
+ #
97
+
98
+ public
99
+ def one(*args)
100
+ self.execute(*args).one
101
+ end
102
+
103
+ ##
104
+ # Returns all selected rows ordered according to datafield from it.
105
+ #
106
+
107
+ public
108
+ def assoc(*specification, &block)
109
+ self.execute.assoc(*specification, &block)
110
+ end
111
+
112
+ ##
113
+ # Returns first value of first row.
114
+ #
115
+
116
+ public
117
+ def single(*args)
118
+ self.execute(*args).single
119
+ end
120
+
121
+ ##
122
+ # Handles iterating.
123
+ #
124
+
125
+ public
126
+ def each(*args)
127
+ self.execute(*args).each do |item|
128
+ yield item
129
+ end
130
+ end
131
+
132
+ ##
133
+ # Maps callback to array.
134
+ #
135
+
136
+ public
137
+ def map(*args, &block)
138
+ result = [ ]
139
+
140
+ self.each(*args) do |item|
141
+ result << block.call(item)
142
+ end
143
+
144
+ return result
145
+ end
146
+
147
+ ##
148
+ # Returns only rows for which block is true.
149
+ #
150
+
151
+ public
152
+ def find_all(*args, &block)
153
+ result = [ ]
154
+
155
+ self.each(*args) do |item|
156
+ if block.call(item) === true
157
+ result << block.call(item)
158
+ end
159
+ end
160
+
161
+ return result
162
+ end
163
+
164
+ alias :filter :find_all
165
+
166
+ end
167
+ end
168
+ end
169
+
@@ -0,0 +1,51 @@
1
+ # encoding: utf-8
2
+ require "fluent-query/queries/abstract"
3
+
4
+ module FluentQuery
5
+ module Queries
6
+
7
+ ##
8
+ # Compiled query.
9
+ #
10
+
11
+ class Compiled < FluentQuery::Queries::Abstract
12
+
13
+ ##
14
+ # Holds query in compiled form.
15
+ #
16
+
17
+ @raw
18
+ attr_accessor :raw
19
+
20
+ ##
21
+ # Constructor.
22
+ #
23
+
24
+ public
25
+ def initialize(connection, query)
26
+ super(connection)
27
+ @raw = query.processor.compile(@connection.driver.build_query(query, :compile))
28
+ end
29
+
30
+ ##
31
+ # Builds prepared query string to final form.
32
+ #
33
+
34
+ public
35
+ def build(*args)
36
+ @raw.complete(*args)
37
+ end
38
+
39
+ ##
40
+ # Returns all selected rows ordered according to datafield from it.
41
+ #
42
+
43
+ public
44
+ def assoc(specification, *args)
45
+ self.execute(*args).assoc(specification)
46
+ end
47
+
48
+ end
49
+ end
50
+ end
51
+
@@ -0,0 +1,50 @@
1
+ # encoding: utf-8
2
+ require "fluent-query/queries/abstract"
3
+
4
+ module FluentQuery
5
+ module Queries
6
+
7
+ ##
8
+ # Compiled query.
9
+ #
10
+
11
+ class Prepared < FluentQuery::Queries::Abstract
12
+
13
+ ##
14
+ # Holds query in prepared form.
15
+ #
16
+
17
+ @query
18
+
19
+ ##
20
+ # Constructor.
21
+ #
22
+
23
+ public
24
+ def initialize(connection, query)
25
+ super(connection)
26
+ @query = @connection.driver.prepare(query)
27
+ end
28
+
29
+ ##
30
+ # Builds prepared query string to final form.
31
+ #
32
+
33
+ public
34
+ def build(*args)
35
+ [@query, args]
36
+ end
37
+
38
+ ##
39
+ # Returns all selected rows ordered according to datafield from it.
40
+ #
41
+
42
+ public
43
+ def assoc(specification, *args)
44
+ self.execute(*args).assoc(specification)
45
+ end
46
+
47
+ end
48
+ end
49
+ end
50
+
@@ -0,0 +1,392 @@
1
+ # encoding: utf-8
2
+ require "date"
3
+ require "hash-utils/string"
4
+
5
+ require "fluent-query/query"
6
+ require "fluent-query/compiler"
7
+
8
+ module FluentQuery
9
+ module Queries
10
+
11
+ ##
12
+ # Query processor.
13
+ #
14
+ # Its primary aim is to dive processing methods from query object as is.
15
+ # In fact, defines something as processing session.
16
+ #
17
+
18
+ class Processor
19
+
20
+ ##
21
+ # Indicates column replacement to perform.
22
+ # (Flag.)
23
+ #
24
+
25
+ COLUMN_REPLACEMENT = 2
26
+
27
+ ##
28
+ # Indicates string replacement to perform.
29
+ # (Flag.)
30
+ #
31
+
32
+ STRING_REPLACEMENT = 1
33
+
34
+ ##
35
+ # Indicates formatting directive replacements to perform.
36
+ # (Flag.)
37
+ #
38
+
39
+ FORMATTING_REPLACEMENT = 4
40
+
41
+ ##
42
+ # Describes formatting directive.
43
+ # @var Regexp
44
+ #
45
+
46
+ FORMATTING_DIRECTIVE = /%%(?:[islbfdt]|sql|and|or)(?:[^\w]|$)/
47
+
48
+ ##
49
+ # Describes column directive.
50
+ # @var Regexp
51
+ #
52
+
53
+ COLUMN_DIRECTIVE = /(?:^|\.|[^\'"\w\\])?\[(?:[\w\.]*[^\\\W])\](?:[^\'"\w]|\.|$)/ # problem is, it interpretes regex
54
+ # from left to right, so if second
55
+ # occurence is immedieately
56
+ # it will not be matched -- that
57
+ # is reason for ? in the begin section
58
+
59
+ ##
60
+ # Describes string directive.
61
+ # @var Regexp
62
+ #
63
+
64
+ STRING_DIRECTIVE = /(?:^|[^\\])"/
65
+
66
+ ##
67
+ # Holds simple column definition.
68
+ # @var Regexp
69
+ #
70
+
71
+ FORMATTING_DIRECTIVE_SIMPLE = /%%\w+/
72
+
73
+ ##
74
+ # Holds simple column definition.
75
+ #
76
+
77
+ COLUMN_DIRECTIVE_SIMPLE = /\[.+\]/
78
+
79
+ ##
80
+ # Final regexp for incremental fluent expanding.
81
+ # (internal cache)
82
+
83
+ @__fluent_expander
84
+
85
+ ##
86
+ # Final regexp for incremental fluent replacing.
87
+ # (internal cache)
88
+
89
+ @__fluent_replacer
90
+
91
+ ##
92
+ # Driver upon which processor should work upon.
93
+ #
94
+
95
+ @driver
96
+ attr_reader :driver
97
+
98
+ ##
99
+ # Compiler associated to this processor.
100
+ #
101
+
102
+ @_compiler
103
+
104
+ ##
105
+ # Constructs processor.
106
+ # @param MP_Fluent_Driver driver Driver upon which processor should work upon.
107
+ #
108
+
109
+ public
110
+ def initialize(driver)
111
+ @driver = driver
112
+ end
113
+
114
+ ##
115
+ # Process array to some reasonable string form.
116
+ #
117
+ # Joins its elements and separates them by commas. Quotes strings
118
+ # by by-driver-defined string quoting and integers by integer quoting.
119
+ #
120
+
121
+ public
122
+ def process_array(array, glue = ",")
123
+ result = [ ]
124
+
125
+ array.each do |item|
126
+ result << self.quote_value(item)
127
+ end
128
+
129
+ return result.join(glue + " ")
130
+ end
131
+
132
+ ##
133
+ # Process hash to some reasonable string form.
134
+ #
135
+ # Joins its elements to name = value form and separates them
136
+ # by commas. Quotes string value by by-driver-defined string quoting
137
+ # and integers by integer quoting. Quotes name by identifiers quoting.
138
+ #
139
+ # Handles two modes:
140
+ # * "assigning" which classicaly keeps for example the '=' operator,
141
+ # * "comparing" which sets for example 'IS' operator for booleans.
142
+ #
143
+
144
+ public
145
+ def process_hash(hash, glue = ",", equality = :comparing)
146
+ result = [ ]
147
+ mode = equality
148
+
149
+ hash.each_pair do |key, value|
150
+ operator = @driver.quote_equality(value, mode)
151
+ key = self.quote_identifier(key)
152
+ value = self.quote_value(value)
153
+
154
+ result << (key + " " + operator + " " + value)
155
+ end
156
+
157
+ return result.join(" " << glue << " ")
158
+ end
159
+
160
+ ##
161
+ # Quotes string by driver string quoting and integers by integer
162
+ # quoting. Other values are converted to string too and
163
+ # leaved unquoted.
164
+ #
165
+
166
+ public
167
+ def quote_value(value)
168
+
169
+ if (value.string?) or (value.symbol?)
170
+ result = @driver.quote_string(value.to_s)
171
+ elsif value.kind_of? Integer
172
+ result = @driver.quote_integer(value)
173
+ elsif value.array?
174
+ result = "(" << self.process_array(value) << ")" # TODO: question is, if we should do it here, if it's enough general just for processor
175
+ elsif value.kind_of? Float
176
+ result = @driver.quote_float(value)
177
+ elsif (value.kind_of? TrueClass) or (value.kind_of? FalseClass)
178
+ result = @driver.quote_boolean(value)
179
+ elsif (value.kind_of? Date) or (value.kind_of? DateTime)
180
+ result = @driver.quote_date_time(value)
181
+ elsif value.nil?
182
+ result = @driver.null
183
+ else
184
+ result = value.to_s
185
+ end
186
+
187
+ return result
188
+
189
+ end
190
+
191
+ ##
192
+ # Quotes subquery by driver subquery quoting.
193
+ #
194
+
195
+ public
196
+ def quote_subquery(subquery)
197
+ if subquery.kind_of? FluentQuery::Query
198
+ subquery = subquery.build!
199
+ end
200
+
201
+ return @driver.quote_subquery(subquery)
202
+ end
203
+
204
+ ##
205
+ # Quotes identifiers by identifier quoting.
206
+ #
207
+
208
+ public
209
+ def quote_identifier(identifier)
210
+ @driver.quote_identifier(identifier.to_s)
211
+ end
212
+
213
+ ##
214
+ # Quotes identifiers list.
215
+ #
216
+
217
+ public
218
+ def quote_identifiers(identifiers)
219
+ identifiers.map { |i| self.quote_identifier(i) }
220
+ end
221
+
222
+ ##
223
+ # Processes identifier list to the string representation.
224
+ # Quotes them and joins them separated by commas.
225
+ #
226
+
227
+ public
228
+ def process_identifiers(identifiers)
229
+ self.quote_identifiers(identifiers).join(", ")
230
+ end
231
+
232
+ ##
233
+ # Processed strings with format definitions and data specifications.
234
+ #
235
+ # Mode can be :compile, :build or :finish. Compiling means building the
236
+ # query without expanding the formatting directives. Finishing means
237
+ # building the prepared query.
238
+ #
239
+
240
+ public
241
+ def process_formatted(sequence, mode = :build)
242
+ count = sequence.length
243
+ i = 0
244
+ output = ""
245
+
246
+ replacer_settings = self.class::COLUMN_REPLACEMENT | self.class::STRING_REPLACEMENT
247
+ expander_settings = self.class::FORMATTING_REPLACEMENT
248
+
249
+ while i < count
250
+ item = sequence[i]
251
+
252
+ ##
253
+
254
+ if item.kind_of? String
255
+ item = item.dup
256
+
257
+ # Calls to each occurence of directive matching to directive
258
+ # finding expression directive processing. In each call increases
259
+ # sequence position counter so moves forward.
260
+
261
+ if mode != :finish
262
+ item.gsub!(self.replacer) do |directive|
263
+ directive.strip!
264
+ self.process_directive(directive, nil, replacer_settings)
265
+ end
266
+ end
267
+
268
+ if mode != :compile
269
+ item.gsub!(self.expander) do |directive|
270
+ self.process_directive(directive, sequence[i += 1], expander_settings)
271
+ end
272
+ end
273
+
274
+ output << item
275
+
276
+ elsif item.kind_of? Symbol
277
+ output << self.quote_identifier(item)
278
+
279
+ else
280
+ output << item.to_s
281
+
282
+ end
283
+
284
+ output << " "
285
+ i += 1
286
+ end
287
+
288
+ return output
289
+
290
+ end
291
+
292
+ ##
293
+ # Processes fluent directive.
294
+ # Replacements is three bytes flag array.
295
+ #
296
+
297
+ public
298
+ def process_directive(directive, argument = nil, replacements = 7)
299
+ if (replacements & self.class::COLUMN_REPLACEMENT > 0) and (directive[0].ord == 91) # "[", Column directive
300
+ result = directive.gsub(self.class::COLUMN_DIRECTIVE_SIMPLE) { |value| self.quote_identifier(value[1..-2]) }
301
+ elsif (replacements & self.class::STRING_REPLACEMENT > 0) and (directive[-1].ord == 34) # "\"", String directive
302
+ result = directive.gsub('"', @driver.quote_string("").last)
303
+ elsif (replacements & self.class::FORMATTING_REPLACEMENT > 0) and (directive[0..1].to_sym == :"%%") # Formatting directive
304
+ result = directive.gsub(self.class::FORMATTING_DIRECTIVE_SIMPLE) { |value| self.process_formatting(value[2..-1], argument) }
305
+ else
306
+ result = directive
307
+ end
308
+
309
+ return result
310
+ end
311
+
312
+ ##
313
+ # Processes formatting directive.
314
+ #
315
+
316
+ public
317
+ def process_formatting(directive, argument)
318
+ proc = self.compiler.compile_formatting(directive)
319
+
320
+ if proc
321
+ output = proc.call(argument)
322
+ else
323
+ output = ""
324
+ end
325
+
326
+ return output
327
+ end
328
+
329
+ ##
330
+ # Returns NULL representation.
331
+ #
332
+
333
+ public
334
+ def null
335
+ @driver.null
336
+ end
337
+
338
+ ##
339
+ # Returns final fluent expander query.
340
+ #
341
+
342
+ public
343
+ def expander
344
+ if @__fluent_expander.nil?
345
+ @__fluent_expander = self.class::FORMATTING_DIRECTIVE
346
+ end
347
+
348
+ @__fluent_expander
349
+ end
350
+
351
+ ##
352
+ # Returns final fluent formatter query.
353
+ #
354
+
355
+ public
356
+ def replacer
357
+ if @__fluent_replacer.nil?
358
+ parts = Array[self.class::COLUMN_DIRECTIVE, self.class::STRING_DIRECTIVE]
359
+
360
+ result = ""
361
+ result << "(?:(?:" << parts.join(")|(?:") << "))"
362
+
363
+ @__fluent_replacer = Regexp::new(result)
364
+ end
365
+
366
+ @__fluent_replacer
367
+ end
368
+
369
+ ##
370
+ # Returns compiler.
371
+ #
372
+
373
+ def compiler
374
+ if @_compiler.nil?
375
+ @_compiler = FluentQuery::Compiler::new(self)
376
+ end
377
+
378
+ @_compiler
379
+ end
380
+
381
+ ##
382
+ # Compiles the string.
383
+ #
384
+
385
+ def compile(string)
386
+ self.compiler.compile(string)
387
+ end
388
+
389
+ end
390
+ end
391
+ end
392
+