-
Notifications
You must be signed in to change notification settings - Fork 128
/
querygenerator.d
552 lines (499 loc) · 24.3 KB
/
querygenerator.d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
/**
* Create SQL queries from string containing D expressions.
* This module allows you to generate SQL from a template, containing D expressions. Depending on these expressions
* parts of the template will be included in the SQL or not.
* Authors: Robert Klotzner, robert.klotzner at gmail.com
* Date: January 7, 2013
* License: GNU General Public License version 3 <http://www.gnu.org/licenses/>
* Copyright: 2013 Robert Klotzner
* Bugs: Unicode not really supported. I haven't really explored that yet, but
* I believe that at the moment string processing will fail for multibyte
* utf-8 characters in the input string.
*/
module arsd.querygenerator;
import std.exception;
import std.uni;
import std.string;
import std.variant;
import std.conv;
import std.typetuple;
/**
* The generated query + args.
*
* It offers support for concatenation, so you can create your query in parts and concatenate them afterwards.
* Ths string prepend will be inserted inbetween two CreatedQuery.query strings at concatation if and only if both query strings are non empty.
* The resulting CreatedQuery has a prepend string equal to the one of the left side of the '~' operation.
* Beware that because of the handling of the prepend string, the result of multiple concatenations depends on the order of evaluation (if empty strings are involved).
* You might need parantheses or splitting
* it up in multiple statements to achieve the desired effect.
*/
struct CreatedQuery {
/**
* Concatenation with automatically inserted prepend string in between.
* The prepend string gets inserted inbetween the two query strings if and only if both query strings are non empty.
* The prepend string of the resulting object is the one of the left object. Thus the resulting object depends on the order of execution if empty strings are involved.
* See the unittest to this struct for details.
*/
CreatedQuery opBinary(string op)(CreatedQuery right) if(op=="~") {
CreatedQuery res=this;
res~=right;
return res;
}
CreatedQuery opBinary(string op)(string right) if(op=="~") {
CreatedQuery res=this;
res.query~=right;
return res;
}
CreatedQuery opBinaryRight(string op)(string left) if(op=="~") {
CreatedQuery res=this;
if(left.length && query.length)
left~=prepend;
res.query=left~res.query;
return res;
}
ref CreatedQuery opOpAssign(string op)(string right) if(op=="~") {
query~=right;
return this;
}
ref CreatedQuery opOpAssign(string op)(CreatedQuery right) if(op=="~") {
query~=(query.length && right.query.length) ? right.prepend : "";
query~=right.query;
args~=right.args;
return this;
}
/// Currently not const because of bug:
/// http://d.puremagic.com/issues/show_bug.cgi?id=8759
bool opEquals( CreatedQuery other) {
return query==other.query && args==other.args && prepend==other.prepend;
}
string query;
Variant[] args;
string prepend;
}
unittest {
auto q1=CreatedQuery("select * from table");
CreatedQuery q2=CreatedQuery("", [], " where ");
auto q3=CreatedQuery("col1=? and col2=?", [Variant(7), Variant("huhu")], " and ");
auto q4=(q1~(q2~q3));
q1~=q2~q3;
import std.stdio;
writefln("q4: %s, q1: %s", q4, q1);
assert(q4==q1);
assert(q1==CreatedQuery("select * from table where col1=? and col2=?", [Variant(7), Variant("huhu")], ""));
}
/**
* This method generates D code from the preSql data, which can be mixed in for generating the resulting sql and the resulting Variant[] array.
* The variable sql_ will contain the generated SQL.
* The variable args_ is the Variant[] array.
* Use createQuery for directly generating the resulting SQL and args.
* Params:
* preSql = The to be processed SQL containing D expressions in #{} blocks, sub blocks in {} and declarations in ${}.
* Returns: D code which can be mixed in.
**/
string createQueryGenerator(string preSql) {
string out_cmd;
out_cmd~="string sql_;\n";
out_cmd~="Variant[] args_;\n";
out_cmd~="with(data_) {\n";
out_cmd~=preSqlParser(preSql);
out_cmd~="\n}";
return out_cmd;
}
/**
* Uses createQueryGenerator for actually doing the job.
* data_ will be made available with D's 'with' statement to the embedded code in queryString. So you can access the elements just as regular variables.
*
* Params:
* queryString = An SQL template. An SQL template consists of nested blocks. The
* uppermost block is the queryString itself, you create subblocks by
* enclosing it in braces '{}'.
__Declaration part__
Each block might start with a declaration part. It does
so if the first character in the block is a '$'. The
declaration part ends with a colon ':'.
It consists of declarations which start with '${' and end
with '}'. Two types of declarations are allowed:
1. variable assignment of the form: ${a=5} This will
result in a D expression in the generated code of the
form 'auto a=5;'.
2. Foreach loop declaration. This one might only occur
once in the declaration part and causes the block in
which it is declared to be executed in a loop.
It looks like: ${a in someArray}
3. Special assignment of ${queryGenSep=" or "} : This one
is specially treated. If you specified a loop the
generated string of each iteration will be concatenated
and the queryGenSep will be used to separate them. If you
don't specify one, the default will be " or ".
Variable assignments before the optional loop declaration
will be declared outside the loop, the ones after the
loop declaration will be declared within the loop and
might access the loop variable, ('a' in our case).
Multiple declarations of the form '${}' might be present
within the declaration part, they may be separated by
white space.
__Body part__
If no declaration part is present, then the whole content
of the block is the body part. (This is, if the first
character is no '$'.) Otherwise it starts after the colon
at the end of the declaration part.
Everything in the body part will be echoed to the
resulting SQL so you can just write plain SQL now. There
are only a few exceptions:
1. A '{' will introduce a child block.
2. A '#{' will start a D expression, which ends with
another '}'
3. A '$' will trigger an error as it is only valid in the
declaration part.
4. SQL string which can either start with " or with '. It
might contain any of '{' '}' '$' '#' they all will be
ignored within a string. Apart from this, no escaping of
these characters is possible at the moment.
__D expression__
A D expression might occur any amount of times within the
body part of a block. It contents will be evaluated as D
code. Which must evaluate to some value. If this value is
different from the value's types init value it is
considered valid, then it will be included in the Variant[] array and a '?'
gets inserted into the resulting SQL. If the returned value equals the type's
init value then no '?' gets inserted and the value will not be present in the
args array.
So if the D expression evaluates to a string and is the
empty string, it will not be included.
The expression is contained within #{ D expression }.
The D code has access to the passed params via params[0]
.. params[n], all declarations made in the declarations
parts of the blocks above it and the contents of the data_ struct.
It is made available with D's 'with' statement so
data_.foo will just be 'foo'.
__Blocks__
Blocks are the building blocks of the resulting
expression. If a block contains a D expression or a
subblock which contains D expressions, then its contents
will only be included in the output if at least one of
the D expressions are valid or if it contains a text only sub block.
So if you have a SQL expression of the
form {someColumn=#{some D code}} there will be no output
at all if #{some D code} was not valid. If a block does
not contain any D code and neither do any sub blocks then
its output will be included in the output of the
containing block. If you want to ensure that a particular
text does not get dropped, include it in its own block.
The second property that makes blocks useful is that if a
block produces no output, for the above mentioned
reasons, then the text before it will be dropped if there
were no valid sub blocks found already in the parent block.
It will also be dropped if the preceding block vanished. This way you can
concatenate blocks with ' and ' and ' or ' and they will
be dropped if not applicable. A leading ' where ' on the
other hand would not be dropped in any case, except the
whole output vanishes.
For examples, see the unittests, a more complex example, I used in reality is here:
`({${db=params[0]} : `
`{`
`${d in datespan} ${arData=db.autoRunData(d.autorun)}: {({date>=#{d.from}} and {date<=#{d.to}} and `
`{({task_run.id>=#{arData.firstId==long.max ? 0 : arData.firstId}} and {task_run.id<=#{arData.lastId}} and {comment like #{d.autorun.length ? "%:Autorun:"~d.autorun~":Autorun:%" : ""}})})}`
`}`
`})`
Right at the start I am starting the uppermost block with a
'(' which means it has no declaration part, then I start
the first sub block, followed by declaration I needed.
Then I start another block with '{' followed by a loop
declaration (d will iterate over the datespan array which
comes from the data_ struct) followed by an inner
variable declaration of arData, you can see both d (the
looping variable) and db from the outer block are
accessible. Afterwards the body starts with a
block containing '(' and subblocks of comparisons with a D
expression like {({date>=#{d.from}})}.
If all D expressions in the block like d.from evaluate to
an empty string, the whole containing block will not produce
any output. So not even '()' this is because they are
enclused in a subblock. The 'and' between {date>=#{d.from}}
and {date<=#{d.to}} for example will only appear in the
output if both d.from and d.to contained a valid non empty
string.
The outputs of each loop iteration will be separated with " or " by default,
you can change this, by setting queryGenSep in the declaration part.
data_ = The contents of data_ will be made available to the D expressions and declarations in the queryString. (The code gets embedded withing a with(data_) { ... })
params = Additional data you need in the queryString. You can access it via params[0], params[1], ...
If you happen to have a member of data_ which is named params, you will have to access it via data_.params.
*/
CreatedQuery createQuery(string queryString, StructT, Params...)(StructT data_, Params params) {
debug(queryGenerator) import std.stdio;
mixin(createQueryGenerator(queryString));
debug(queryGenerator) writeln("Generated: ", createQueryGenerator(queryString));
return CreatedQuery(sql_, args_);
//return CreatedQuery.init;
}
unittest {
import std.stdio;
struct Test1 {
string a;
string b;
}
struct Test {
Test1[] foo;
string bar;
int k;
}
Test t1;
CreatedQuery myQuery(Test t1) {
return createQuery!`select * from testtable where
{({${queryGenSep=" or "} ${f in foo} : ({col1=#{f.a}} and {col2=#{f.b}})})} or
{col3>#{k}}`(t1);
}
auto res=myQuery(t1);
writeln("Result with empty args: ", res.query);
assert(res.args==[]);
t1.k=9;
res=myQuery(t1);
writefln("Result with k set: %s, args: %s", res.query, res.args);
assert(res.args==[Variant(9)]);
t1.foo~=Test1(string.init, "Hallo du da!");
res=myQuery(t1);
writeln("Result with foo.b and k set: ", res.query);
assert(res.args==[Variant("Hallo du da!"), Variant(9)]);
t1.foo~=Test1("Hallo du da!", string.init);
res=myQuery(t1);
writeln("Result with foo0.b and foo1.a and k set: ", res.query);
assert(res.args==[Variant("Hallo du da!"), Variant("Hallo du da!"), Variant(9)]);
t1.foo~=Test1("Hello!", "Cu!");
res=myQuery(t1);
writeln("Result with foo0.b and foo1.a and foo2.a and foo2.b and k set: ", res.query);
assert(res.args==[Variant("Hallo du da!"), Variant("Hallo du da!"), Variant("Hello!"), Variant("Cu!"), Variant(9)]);
}
private:
void dropWhite(ref string buf) {
buf=buf.stripLeft();
}
/// D's indexOf seemed not to work at compile time.
size_t indexOf(string heap, char needle) {
foreach(i, c; heap) {
if(needle==c) {
return i;
}
}
return -1;
}
//pragma(msg, createQueryGenerator( " (${item in datespan} : ( { date>=#{item.from} and} {date<=#{item.to} and} {comment like #{\"%:Autorun:\"~item.autorun~\":Autorun:%\"}})"));
//pragma(msg, createQueryGenerator( " Hello this is a test!"));
//pragma(msg, createQueryGenerator( ` ${item in datespan} : ( { date>=#{item.from} } and {date<=#{item.to} } and {comment like #{"%:Autorun:"~item.autorun~":Autorun:%"}})`));
//pragma(msg, createQueryGenerator(`select * from testtable where
//{({${queryGenSep=" or "} ${f in foo} : ({col1=#{f.a}} and {col2=#{f.b}})})} or
//{col3>k}`));
//Syntax:
// ${a=3} ${i in array} ${u=i.c} : some data #{D expression} { same again }
// presql : [decls :] body
// decls : [${variable=assignment}]* [${variable in array}] [${variable=assignment}]*
// body : [some_string | #{D expression} | {presql}]*
/// private
/// Handles the part before the ":" colon. (Variable declarations + loop declaration)
private static string doVariableProcessing(ref string part, int level) {
string output;
string for_each_decl;
string after_output;
bool queryGenSepFound=false;
string buf="buf"~to!string(level);
dropWhite(part);
assert(part);
immutable isFirst="isFirst"~to!string(level);
if(part[0]!='$') {
return "bool "~isFirst~"=false;\n"~"{\nstring "~buf~";\n";
}
output~="bool "~isFirst~"=true;\n";
while(true) {
assert(part.length>2);
assert(part[0]=='$', "Only declarations enclosed in '${' are allowed in declarations block. Invalid data found: "~part);
assert(part[1]=='{', "'{' in '${' is mandatory! Found at:"~part);
string var_name;
part=part[2..$];
foreach(i,c; part) {
if(!c.isAlpha()) {
var_name=part[0..i];
if(var_name=="queryGenSep") {
var_name="queryGenSep"~to!string(level);
queryGenSepFound=true;
}
part=part[i..$];
break;
}
}
dropWhite(part);
assert(part.length, "Unexpected end of data, expected '=' or 'in'");
enum Operation {
assignment,
in_array
}
Operation op;
switch(part[0]) {
case '=': op=Operation.assignment;
part=part[1..$];
break;
case 'i':
assert(part.length>1 && part[1]=='n', "Expected 'n' after 'i' forming 'in', got: "~part);
part=part[2..$];
op=Operation.in_array;
break;
default: assert(false, "Unexpected operation: Only variable assignment ('=') and array loop ('in') are supported, found at: "~part);
}
dropWhite(part);
string right_side;
foreach(i, c; part) {
if(c=='}') {
right_side=part[0..i];
part=part[i+1..$];
break;
}
}
if(op==Operation.assignment) {
string buff="auto "~var_name~"="~right_side~";\n";
if(for_each_decl)
after_output~=buff;
else
output~=buff;
}
else {
if(for_each_decl)
assert(false, "Only one foreach declaration allowed, found second at: "~part);
for_each_decl="\nforeach("~var_name~"; "~right_side~") {\n";
}
dropWhite(part);
if(part[0]==':') {
part=part[1..$];
if(!queryGenSepFound) {
output~="immutable queryGenSep"~to!string(level)~"=\" or \";\n";
}
for_each_decl = for_each_decl==[] ? "{\n" : for_each_decl;
after_output~="string "~buf~";\n";
after_output~="if(!"~isFirst~") {\n";
after_output~=buf~"=queryGenSep"~to!string(level)~";\n}\n";
return output~for_each_decl~after_output;
}
else {
assert(part, "Unexpected end of string, expected another declaration '${}' or ':'.");
assert(part[0]=='$', "Expected ':' or another variable assignment ('$'), at: "~part);
}
}
}
// Extracts a D expression (#{}) from the string.
string dExpressionParser(ref string data, string buf, string validCount, string slevel, ref int count) {
assert(data.length>2 && data[0]=='#' && data[1]=='{');
string out_cmd;
data=data[2..$];
auto end=data.indexOf('}');
assert(end>0, "Empty or non closed D expression found at: "~data);
string val="val"~slevel~"_"~to!string(count);
out_cmd~="auto "~val~"="~data[0..end]~";\n";
out_cmd~="if("~val~"!=typeof("~val~").init) {\n";
out_cmd~="debug(queryGenerator) writeln(\"Found valid value: \", "~val~");\n";
out_cmd~=validCount~"++;\n";
out_cmd~=buf~"~=\"?\";\n";
out_cmd~="args_~=Variant("~val~");\n}\n";
out_cmd~="else {\ndebug(queryGenerator) writeln(\"Found invalid value: \", "~val~");\n}\n";
data=data[end+1..$];
return out_cmd;
}
// do the parsing of an sql string ('....' or ".....") Also handles escaping ('') or ("")
string processString(ref string data, string buf) {
assert(data[0]=='\'' || data[0]=='"', "Expected ' or \"");
char begin=data[0];
data=data[1..$];
string out_cmd;
while(data.length) {
foreach(i, c; data) {
if(c==begin && ((i+1)>=data.length || data[i+1]!=begin) ) { // End of string (Ignore escaped end in string)
out_cmd~=buf~"~=`"~begin~data[0..i]~begin~"`;\n";
data=data[i+1 .. $];
break;
}
}
}
return out_cmd;
}
// Parsing starts here.
string preSqlParser(ref string data, int level=0) {
auto out_cmd="{\n";
out_cmd~=doVariableProcessing(data, level);
debug(querygenerator) import std.stdio;
// dropWhite(data); //(Maybe intended, so don't drop it)
immutable slevel=to!string(level);
immutable buf="buf"~slevel;
immutable text="text"~slevel;
string upperBuf;
string upperWasValid;
int dexprCount=0;
if(level>0) {
upperBuf="buf"~(to!string(level-1));
upperWasValid="wasValid"~(to!string(level-1));
}
else
upperBuf="sql_";
immutable validCount="validCount"~slevel;
immutable wasValid="wasValid"~slevel;
immutable isFirst="isFirst"~to!string(level); // Defined in doVariableProcessing
out_cmd~="int "~validCount~"=-1;\n";
out_cmd~="int "~wasValid~";\n"; // validCount of inner level. (But never -1, if it would be -1 it is 1)
out_cmd~="string "~text~";\n";
while(data.length) {
auto end=data.length;
foreach(i, c; data) {
if(c=='{' || c=='}' || c=='#' || c=='\'' || c=='"' || c=='$') {
end=i;
break;
}
}
out_cmd~=text~"=`"~data[0..end]~"`;\n";
out_cmd~=buf~"~=`"~data[0..end]~"`;\n";
data=data[end..$];
if(data.length==0)
break;
debug(querygenerator) writefln("Remaining (level: %s) data: %s", level, data);
switch(data[0]) {
case '{' :
assert(data.length>2, "Expected some data after '{' at: "~data);
data=data[1..$];
out_cmd~="if("~validCount~"==0) {\n";
out_cmd~=buf~"="~buf~"[0..$-"~text~".length];\n}\n";
out_cmd~=wasValid~"=0;\n"; // Reset, because inner level will just add to it.
out_cmd~=preSqlParser(data, level+1);
assert(data[0]=='}', "Expected closing '}', got: "~data);
data=data[1..$];
out_cmd~="if("~wasValid~"==0 && "~validCount~">0) {\n"; // validCount has to be greater than 0 otherwise we have removed the data already. (See above)
out_cmd~=buf~"="~buf~"[0..$-"~text~".length];\n}\n";
out_cmd~=`debug(queryGenerator) writefln("Received data from level: `~to!string(level+1)~` on level: `~slevel~`, data was valid: %s, current valid count: %s", `~wasValid~`, `~validCount~`);`~"\n";
out_cmd~=validCount~"="~validCount~"==-1 ? "~wasValid~" : "~validCount~"+"~wasValid~";\n";
out_cmd~=`debug(queryGenerator) writefln("Updated valid count is now: %s", `~validCount~`);`~"\n";
break;
case '}' :
goto finish;
case '#' :
out_cmd~="if("~validCount~"==-1) {\n\t";
out_cmd~=validCount~"=0;\n}\n";
out_cmd~=dExpressionParser(data, buf, validCount, slevel, dexprCount);
dexprCount++;
break;
case '\'':
case '"':
out_cmd~=processString(data, buf);
break;
case '$': assert(false, "Unexpected '$'. The variable declaration block must start with a '${}' declaration otherwise you are already in the body and '${}' declarations are invalid.\n Found at: "~data);
default:
assert(false, "WTF? The if should have made this impossible!");
}
}
finish:
out_cmd~=`debug(queryGenerator) writefln("Leaving level: %s, valid count: %s, buffer: %s", `~slevel~`, `~validCount~`, `~buf~" );\n";
out_cmd~="if("~validCount~"==-1 || "~validCount~">0) {\n";
out_cmd~="debug(queryGenerator) writeln(\"Appended "~buf~" with contents: \", "~buf~");\n";
out_cmd~=isFirst~"=false;\n"; // No longer the first valid run, so separator should be inserted.
out_cmd~=upperBuf~"~="~buf~";\n}\n";
if(level>0) {
out_cmd~=upperWasValid~"+="~validCount~"<0 ? 1 : "~validCount~";\n";
}
// End of loop:
out_cmd~="}\n";
// End of block:
out_cmd~="}\n";
return out_cmd;
}