1 /**
2  * DStruct - Object-Relation Mapping for D programming language, with interface similar to Hibernate. 
3  * 
4  * Hibernate documentation can be found here:
5  * $(LINK http://hibernate.org/docs)$(BR)
6  * 
7  * Source file dstruct/core.d.
8  *
9  * This module contains HQL query parser and HQL to SQL transform implementation.
10  * 
11  * Copyright: Copyright 2013
12  * License:   $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
13  * Author:   Vadim Lopatin
14  */
15 module dstruct.query;
16 
17 import std.ascii;
18 import std.algorithm;
19 import std.exception;
20 import std.array;
21 import std..string;
22 import std.conv;
23 import std.stdio;
24 import std.variant;
25 
26 import dstruct.ddbc.core;
27 
28 import dstruct.annotations;
29 import dstruct.metadata;
30 import dstruct.type;
31 import dstruct.core;
32 import dstruct.dialect;
33 import dstruct.dialects.mysqldialect;
34 
35 
36 // For backwards compatibily
37 // 'enforceEx' will be removed with 2.089
38 static if(__VERSION__ < 2080) {
39     alias enforceHelper = enforceEx;
40 } else {
41     alias enforceHelper = enforce;
42 }
43 
44 enum JoinType {
45     InnerJoin,
46     LeftJoin,
47 }
48 
49 class FromClauseItem {
50 	string entityName;
51     const EntityInfo entity;
52 	string entityAlias;
53 	string sqlAlias;
54     int startColumn;
55     int selectedColumns;
56     // for JOINs
57     JoinType joinType = JoinType.InnerJoin;
58     bool fetch;
59     FromClauseItem base;
60     const PropertyInfo baseProperty;
61     string pathString;
62     int index;
63     int selectIndex;
64 
65     string getFullPath() {
66         if (base is null)
67             return entityAlias;
68         return base.getFullPath() ~ "." ~ baseProperty.propertyName;
69     }
70 
71     this(const EntityInfo entity, string entityAlias, JoinType joinType, bool fetch, FromClauseItem base = null, const PropertyInfo baseProperty = null) {
72         this.entityName = entity.name;
73         this.entity = entity;
74         this.joinType = joinType;
75         this.fetch = fetch;
76         this.base = base;
77         this.baseProperty = baseProperty;
78         this.selectIndex = -1;
79     }
80 
81 }
82 
83 class FromClause {
84     FromClauseItem[] items;
85     FromClauseItem add(const EntityInfo entity, string entityAlias, JoinType joinType, bool fetch, FromClauseItem base = null, const PropertyInfo baseProperty = null) {
86         FromClauseItem item = new FromClauseItem(entity, entityAlias, joinType, fetch, base, baseProperty);
87         item.entityAlias = entityAlias is null ? "_a" ~ to!string(items.length + 1) : entityAlias;
88         item.sqlAlias = "_t" ~ to!string(items.length + 1);
89         item.index = cast(int)items.length;
90         item.pathString = item.getFullPath();
91         items ~= item;
92         return item;
93     }
94     @property size_t length() { return items.length; }
95     string getSQL() {
96         return "";
97     }
98     @property FromClauseItem first() {
99         return items[0];
100     }
101     FromClauseItem opIndex(int index) {
102         enforceHelper!DStructException(index >= 0 && index < items.length, "FromClause index out of range: " ~ to!string(index));
103         return items[index];
104     }
105     FromClauseItem opIndex(string aliasName) {
106         return findByAlias(aliasName);
107     }
108     bool hasAlias(string aliasName) {
109         foreach(ref m; items) {
110             if (m.entityAlias == aliasName)
111                 return true;
112         }
113         return false;
114     }
115     FromClauseItem findByAlias(string aliasName) {
116         foreach(ref m; items) {
117             if (m.entityAlias == aliasName)
118                 return m;
119         }
120         throw new QuerySyntaxException("Cannot find FROM alias by name " ~ aliasName);
121     }
122     FromClauseItem findByPath(string path) {
123         foreach(ref m; items) {
124             if (m.pathString == path)
125                 return m;
126         }
127         return null;
128     }
129 }
130 
131 struct OrderByClauseItem {
132 	FromClauseItem from;
133 	PropertyInfo prop;
134 	bool asc;
135 }
136 
137 struct SelectClauseItem {
138 	FromClauseItem from;
139 	PropertyInfo prop;
140 }
141 
142 class QueryParser {
143 	string query;
144 	EntityMetaData metadata;
145 	Token[] tokens;
146     FromClause fromClause;
147 	//FromClauseItem[] fromClause;
148 	string[] parameterNames;
149 	OrderByClauseItem[] orderByClause;
150 	SelectClauseItem[] selectClause;
151 	Token whereClause; // AST for WHERE expression
152 	
153 	
154 	this(EntityMetaData metadata, string query) {
155 		this.metadata = metadata;
156 		this.query = query;
157         fromClause = new FromClause();
158 		//writeln("tokenizing query: " ~ query);
159 		tokens = tokenize(query);
160         //writeln("parsing query: " ~ query);
161         parse();
162         //writeln("parsing done");
163     }
164 	
165 	void parse() {
166 		processParameterNames(0, cast(int)tokens.length); // replace pairs {: Ident} with single Parameter token
167 		int len = cast(int)tokens.length;
168 		//writeln("Query tokens: " ~ to!string(len));
169 		int fromPos = findKeyword(KeywordType.FROM);
170 		int selectPos = findKeyword(KeywordType.SELECT);
171 		int wherePos = findKeyword(KeywordType.WHERE);
172 		int orderPos = findKeyword(KeywordType.ORDER);
173 		enforceHelper!QuerySyntaxException(fromPos >= 0, "No FROM clause in query " ~ query);
174 		enforceHelper!QuerySyntaxException(selectPos <= 0, "SELECT clause should be first - invalid query " ~ query);
175 		enforceHelper!QuerySyntaxException(wherePos == -1 || wherePos > fromPos, "Invalid WHERE position in query " ~ query);
176 		enforceHelper!QuerySyntaxException(orderPos == -1 || (orderPos < tokens.length - 2 && tokens[orderPos + 1].keyword == KeywordType.BY), "Invalid ORDER BY in query " ~ query);
177 		enforceHelper!QuerySyntaxException(orderPos == -1 || orderPos > fromPos, "Invalid ORDER BY position in query " ~ query);
178 		int fromEnd = len;
179 		if (orderPos >= 0)
180 			fromEnd = orderPos;
181 		if (wherePos >= 0)
182 			fromEnd = wherePos;
183 		int whereEnd = wherePos < 0 ? -1 : (orderPos >= 0 ? orderPos : len);
184 		int orderEnd = orderPos < 0 ? -1 : len;
185 		parseFromClause(fromPos + 1, fromEnd);
186 		if (selectPos == 0 && selectPos < fromPos - 1)
187 			parseSelectClause(selectPos + 1, fromPos);
188 		else
189 			defaultSelectClause();
190 		bool selectedEntities = validateSelectClause();
191 		if (wherePos >= 0 && whereEnd > wherePos)
192 			parseWhereClause(wherePos + 1, whereEnd);
193 		if (orderPos >= 0 && orderEnd > orderPos)
194 			parseOrderClause(orderPos + 2, orderEnd);
195         if (selectedEntities) {
196             processAutoFetchReferences();
197             prepareSelectFields();
198         }
199 	}
200 	
201     private void prepareSelectFields() {
202         int startColumn = 1;
203         for (int i=0; i < fromClause.length; i++) {
204             FromClauseItem item = fromClause[i];
205             if (!item.fetch)
206                 continue;
207             int count = item.entity.metadata.getFieldCount(item.entity, false);
208             if (count > 0) {
209                 item.startColumn = startColumn;
210                 item.selectedColumns = count;
211                 startColumn += count;
212             }
213         }
214     }
215 
216     private void processAutoFetchReferences() {
217         FromClauseItem a = selectClause[0].from;
218         a.fetch = true;
219         processAutoFetchReferences(a);
220     }
221 
222     private FromClauseItem ensureItemFetched(FromClauseItem a, const PropertyInfo p) {
223         FromClauseItem res;
224         string path = a.pathString ~ "." ~ p.propertyName;
225         //writeln("ensureItemFetched " ~ path);
226         res = fromClause.findByPath(path);
227         if (res is null) {
228             // autoadd join
229             assert(p.referencedEntity !is null);
230             res = fromClause.add(p.referencedEntity, null, p.nullable ? JoinType.LeftJoin : JoinType.InnerJoin, true, a, p);
231         } else {
232             // force fetch
233             res.fetch = true;
234         }
235         bool selectFound = false;
236         foreach(s; selectClause) {
237             if (s.from == res) {
238                 selectFound = true;
239                 break;
240             }
241         }
242         if (!selectFound) {
243             SelectClauseItem item;
244             item.from = res;
245             item.prop = null;
246             selectClause ~= item;
247         }
248         return res;
249     }
250 
251     private bool isBackReferenceProperty(FromClauseItem a, const PropertyInfo p) {
252         if (a.base is null)
253             return false;
254         auto baseEntity = a.base.entity;
255         assert(baseEntity !is null);
256         if (p.referencedEntity != baseEntity)
257             return false;
258 
259         if (p.referencedProperty !is null && p.referencedProperty == a.baseProperty)
260             return true;
261         if (a.baseProperty.referencedProperty !is null && p == a.baseProperty.referencedProperty)
262             return true;
263         return false;
264     }
265 
266     private void processAutoFetchReferences(FromClauseItem a) {
267         foreach (p; a.entity.properties) {
268             if (p.lazyLoad)
269                 continue;
270             if (p.oneToOne && !isBackReferenceProperty(a, p)) {
271                 FromClauseItem res = ensureItemFetched(a, p);
272                 processAutoFetchReferences(res);
273             }
274         }
275     }
276     
277     private void updateEntity(const EntityInfo entity, string name) {
278 		foreach(t; tokens) {
279 			if (t.type == TokenType.Ident && t.text == name) {
280                 t.entity = cast(EntityInfo)entity;
281 				t.type = TokenType.Entity;
282 			}
283 		}
284 	}
285 	
286 	private void updateAlias(const EntityInfo entity, string name) {
287 		foreach(t; tokens) {
288 			if (t.type == TokenType.Ident && t.text == name) {
289                 t.entity = cast(EntityInfo)entity;
290 				t.type = TokenType.Alias;
291 			}
292 		}
293 	}
294 	
295 	private void splitCommaDelimitedList(int start, int end, void delegate(int, int) callback) {
296 		//writeln("SPLIT " ~ to!string(start) ~ " .. " ~ to!string(end));
297 		int len = cast(int)tokens.length;
298 		int p = start;
299 		for (int i = start; i < end; i++) {
300 			if (tokens[i].type == TokenType.Comma || i == end - 1) {
301 				enforceHelper!QuerySyntaxException(tokens[i].type != TokenType.Comma || i != end - 1, "Invalid comma at end of list" ~ errorContext(tokens[start]));
302 				int endp = i < end - 1 ? i : end;
303 				enforceHelper!QuerySyntaxException(endp > p, "Invalid comma delimited list" ~ errorContext(tokens[start]));
304 				callback(p, endp);
305 				p = i + 1;
306 			}
307 		}
308 	}
309 
310     private int parseFieldRef(int start, int end, ref string[] path) {
311         int pos = start;
312         while (pos < end) {
313             if (tokens[pos].type == TokenType.Ident || tokens[pos].type == TokenType.Alias) {
314                 enforceHelper!QuerySyntaxException(path.length == 0 || tokens[pos].type != TokenType.Alias, "Alias is allowed only as first item" ~ errorContext(tokens[pos]));
315                 path ~= tokens[pos].text;
316                 pos++;
317                 if (pos == end || tokens[pos].type != TokenType.Dot)
318                     return pos;
319                 if (pos == end - 1 || tokens[pos + 1].type != TokenType.Ident)
320                     return pos;
321                 pos++;
322             } else {
323                 break;
324             }
325         }
326         enforceHelper!QuerySyntaxException(tokens[pos].type != TokenType.Dot, "Unexpected dot at end in field list" ~ errorContext(tokens[pos]));
327         enforceHelper!QuerySyntaxException(path.length > 0, "Empty field list" ~ errorContext(tokens[pos]));
328         return pos;
329     }
330 	
331     private void parseFirstFromClause(int start, int end, out int pos) {
332         enforceHelper!QuerySyntaxException(start < end, "Invalid FROM clause " ~ errorContext(tokens[start]));
333         // minimal support:
334         //    Entity
335         //    Entity alias
336         //    Entity AS alias
337         enforceHelper!QuerySyntaxException(tokens[start].type == TokenType.Ident, "Entity name identifier expected in FROM clause" ~ errorContext(tokens[start]));
338         string entityName = cast(string)tokens[start].text;
339         auto ei = metadata.findEntity(entityName);
340         updateEntity(ei, entityName);
341         string aliasName = null;
342         int p = start + 1;
343         if (p < end && tokens[p].type == TokenType.Keyword && tokens[p].keyword == KeywordType.AS)
344             p++;
345         if (p < end) {
346             enforceHelper!QuerySyntaxException(tokens[p].type == TokenType.Ident, "Alias name identifier expected in FROM clause" ~ errorContext(tokens[p]));
347             aliasName = cast(string)tokens[p].text;
348             p++;
349         }
350         if (aliasName != null)
351             updateAlias(ei, aliasName);
352         fromClause.add(ei, aliasName, JoinType.InnerJoin, false);
353         pos = p;
354     }
355 
356     void appendFromClause(Token context, string[] path, string aliasName, JoinType joinType, bool fetch) {
357         int p = 0;
358         enforceHelper!QuerySyntaxException(fromClause.hasAlias(path[p]), "Unknown alias " ~ path[p] ~ " in FROM clause" ~ errorContext(context));
359         FromClauseItem baseClause = findFromClauseByAlias(path[p]);
360         //string pathString = path[p];
361         p++;
362         while(true) {
363             auto baseEntity = baseClause.entity;
364             enforceHelper!QuerySyntaxException(p < path.length, "Property name expected in FROM clause" ~ errorContext(context));
365             string propertyName = path[p++];
366             auto property = baseEntity[propertyName];
367             auto referencedEntity = property.referencedEntity;
368             assert(referencedEntity !is null);
369             enforceHelper!QuerySyntaxException(!property.simple, "Simple property " ~ propertyName ~ " cannot be used in JOIN" ~ errorContext(context));
370             enforceHelper!QuerySyntaxException(!property.embedded, "Embedded property " ~ propertyName ~ " cannot be used in JOIN" ~ errorContext(context));
371             bool last = (p == path.length);
372             FromClauseItem item = fromClause.add(referencedEntity, last ? aliasName : null, joinType, fetch, baseClause, property);
373             if (last && aliasName !is null)
374                 updateAlias(referencedEntity, item.entityAlias);
375             baseClause = item;
376             if (last)
377                 break;
378         }
379     }
380 
381     void parseFromClause(int start, int end) {
382         int p = start;
383         parseFirstFromClause(start, end, p);
384         while (p < end) {
385             Token context = tokens[p];
386             JoinType joinType = JoinType.InnerJoin;
387             if (tokens[p].keyword == KeywordType.LEFT) {
388                 joinType = JoinType.LeftJoin;
389                 p++;
390             } else if (tokens[p].keyword == KeywordType.INNER) {
391                 p++;
392             }
393             enforceHelper!QuerySyntaxException(p < end && tokens[p].keyword == KeywordType.JOIN, "Invalid FROM clause" ~ errorContext(tokens[p]));
394             p++;
395             enforceHelper!QuerySyntaxException(p < end, "Invalid FROM clause - incomplete JOIN" ~ errorContext(tokens[p]));
396             bool fetch = false;
397             if (tokens[p].keyword == KeywordType.FETCH) {
398                 fetch = true;
399                 p++;
400                 enforceHelper!QuerySyntaxException(p < end, "Invalid FROM clause - incomplete JOIN" ~ errorContext(tokens[p]));
401             }
402             string[] path;
403             p = parseFieldRef(p, end, path);
404             string aliasName;
405             bool hasAS = false;
406             if (p < end && tokens[p].keyword == KeywordType.AS) {
407                 p++;
408                 hasAS = true;
409             }
410             enforceHelper!QuerySyntaxException(p < end && tokens[p].type == TokenType.Ident, "Invalid FROM clause - no alias in JOIN" ~ errorContext(tokens[p]));
411             aliasName = tokens[p].text;
412             p++;
413             appendFromClause(context, path, aliasName, joinType, fetch);
414         }
415 		enforceHelper!QuerySyntaxException(p == end, "Invalid FROM clause" ~ errorContext(tokens[p]));
416 	}
417 	
418 	// in pairs {: Ident} replace type of ident with Parameter 
419 	void processParameterNames(int start, int end) {
420 		for (int i = start; i < end; i++) {
421 			if (tokens[i].type == TokenType.Parameter) {
422 				parameterNames ~= cast(string)tokens[i].text;
423 			}
424 		}
425 	}
426 	
427 	FromClauseItem findFromClauseByAlias(string aliasName) {
428         return fromClause.findByAlias(aliasName);
429 	}
430 	
431 	void addSelectClauseItem(string aliasName, string[] propertyNames) {
432 		//writeln("addSelectClauseItem alias=" ~ aliasName ~ " properties=" ~ to!string(propertyNames));
433 		FromClauseItem from = aliasName == null ? fromClause.first : findFromClauseByAlias(aliasName);
434 		SelectClauseItem item;
435 		item.from = from;
436 		item.prop = null;
437 		EntityInfo ei = cast(EntityInfo)from.entity;
438 		if (propertyNames.length > 0) {
439 			item.prop = cast(PropertyInfo)ei.findProperty(propertyNames[0]);
440 			propertyNames.popFront();
441 			while (item.prop.embedded) {
442 				//writeln("Embedded property " ~ item.prop.propertyName ~ " of type " ~ item.prop.referencedEntityName);
443                 ei = cast(EntityInfo)item.prop.referencedEntity;
444 			    enforceHelper!QuerySyntaxException(propertyNames.length > 0, "@Embedded field property name should be specified when selecting " ~ aliasName ~ "." ~ item.prop.propertyName);
445                 item.prop = cast(PropertyInfo)ei.findProperty(propertyNames[0]);
446 				propertyNames.popFront();
447 			}
448 		}
449 		enforceHelper!QuerySyntaxException(propertyNames.length == 0, "Extra field names in SELECT clause in query " ~ query);
450 		selectClause ~= item;
451 		//insertInPlace(selectClause, 0, item);
452 	}
453 	
454 	void addOrderByClauseItem(string aliasName, string propertyName, bool asc) {
455 		FromClauseItem from = aliasName == null ? fromClause.first : findFromClauseByAlias(aliasName);
456 		OrderByClauseItem item;
457 		item.from = from;
458         item.prop = cast(PropertyInfo)from.entity.findProperty(propertyName);
459 		item.asc = asc;
460 		orderByClause ~= item;
461 		//insertInPlace(orderByClause, 0, item);
462 	}
463 	
464 	void parseOrderByClauseItem(int start, int end) {
465 		// for each comma delimited item
466 		// in current version it can only be
467 		// {property}  or  {alias . property} optionally followed by ASC or DESC
468 		//writeln("ORDER BY ITEM: " ~ to!string(start) ~ " .. " ~ to!string(end));
469 		bool asc = true;
470 		if (tokens[end - 1].type == TokenType.Keyword && tokens[end - 1].keyword == KeywordType.ASC) {
471 			end--;
472 		} else if (tokens[end - 1].type == TokenType.Keyword && tokens[end - 1].keyword == KeywordType.DESC) {
473 			asc = false;
474 			end--;
475 		}
476 		enforceHelper!QuerySyntaxException(start < end, "Empty ORDER BY clause item" ~ errorContext(tokens[start]));
477 		if (start == end - 1) {
478 			// no alias
479 			enforceHelper!QuerySyntaxException(tokens[start].type == TokenType.Ident, "Property name expected in ORDER BY clause" ~ errorContext(tokens[start]));
480 			addOrderByClauseItem(null, cast(string)tokens[start].text, asc);
481 		} else if (start == end - 3) {
482 			enforceHelper!QuerySyntaxException(tokens[start].type == TokenType.Alias, "Entity alias expected in ORDER BY clause" ~ errorContext(tokens[start]));
483 			enforceHelper!QuerySyntaxException(tokens[start + 1].type == TokenType.Dot, "Dot expected after entity alias in ORDER BY clause" ~ errorContext(tokens[start]));
484 			enforceHelper!QuerySyntaxException(tokens[start + 2].type == TokenType.Ident, "Property name expected after entity alias in ORDER BY clause" ~ errorContext(tokens[start]));
485 			addOrderByClauseItem(cast(string)tokens[start].text, cast(string)tokens[start + 2].text, asc);
486 		} else {
487 			//writeln("range: " ~ to!string(start) ~ " .. " ~ to!string(end));
488 			enforceHelper!QuerySyntaxException(false, "Invalid ORDER BY clause (expected {property [ASC | DESC]} or {alias.property [ASC | DESC]} )" ~ errorContext(tokens[start]));
489 		}
490 	}
491 	
492 	void parseSelectClauseItem(int start, int end) {
493 		// for each comma delimited item
494 		// in current version it can only be
495 		// {property}  or  {alias . property}
496 		//writeln("SELECT ITEM: " ~ to!string(start) ~ " .. " ~ to!string(end));
497 		enforceHelper!QuerySyntaxException(tokens[start].type == TokenType.Ident || tokens[start].type == TokenType.Alias, "Property name or alias expected in SELECT clause in query " ~ query ~ errorContext(tokens[start]));
498 		string aliasName;
499 		int p = start;
500 		if (tokens[p].type == TokenType.Alias) {
501             //writeln("select clause alias: " ~ tokens[p].text ~ " query: " ~ query);
502 			aliasName = cast(string)tokens[p].text;
503 			p++;
504 			enforceHelper!QuerySyntaxException(p == end || tokens[p].type == TokenType.Dot, "SELECT clause item is invalid (only  [alias.]field{[.field2]}+ allowed) " ~ errorContext(tokens[start]));
505 			if (p < end - 1 && tokens[p].type == TokenType.Dot)
506 				p++;
507 		} else {
508             //writeln("select clause non-alias: " ~ tokens[p].text ~ " query: " ~ query);
509         }
510 		string[] fieldNames;
511 		while (p < end && tokens[p].type == TokenType.Ident) {
512 			fieldNames ~= tokens[p].text;
513 			p++;
514 			if (p > end - 1 || tokens[p].type != TokenType.Dot)
515 				break;
516 			// skipping dot
517 			p++;
518 		}
519 		//writeln("parseSelectClauseItem pos=" ~ to!string(p) ~ " end=" ~ to!string(end));
520 		enforceHelper!QuerySyntaxException(p >= end, "SELECT clause item is invalid (only  [alias.]field{[.field2]}+ allowed) " ~ errorContext(tokens[start]));
521 		addSelectClauseItem(aliasName, fieldNames);
522 	}
523 	
524 	void parseSelectClause(int start, int end) {
525 		enforceHelper!QuerySyntaxException(start < end, "Invalid SELECT clause" ~ errorContext(tokens[start]));
526 		splitCommaDelimitedList(start, end, &parseSelectClauseItem);
527 	}
528 	
529 	void defaultSelectClause() {
530 		addSelectClauseItem(fromClause.first.entityAlias, null);
531 	}
532 	
533 	bool validateSelectClause() {
534 		enforceHelper!QuerySyntaxException(selectClause != null && selectClause.length > 0, "Invalid SELECT clause");
535 		int aliasCount = 0;
536 		int fieldCount = 0;
537 		foreach(a; selectClause) {
538 			if (a.prop !is null)
539 				fieldCount++;
540 			else
541 				aliasCount++;
542 		}
543 		enforceHelper!QuerySyntaxException((aliasCount == 1 && fieldCount == 0) || (aliasCount == 0 && fieldCount > 0), "You should either use single entity alias or one or more properties in SELECT clause. Don't mix objects with primitive fields");
544         return aliasCount > 0;
545 	}
546 	
547 	void parseWhereClause(int start, int end) {
548 		enforceHelper!QuerySyntaxException(start < end, "Invalid WHERE clause" ~ errorContext(tokens[start]));
549 		whereClause = new Token(tokens[start].pos, TokenType.Expression, tokens, start, end);
550 		//writeln("before convert fields:\n" ~ whereClause.dump(0));
551 		convertFields(whereClause.children);
552 		//writeln("after convertFields before convertIsNullIsNotNull:\n" ~ whereClause.dump(0));
553 		convertIsNullIsNotNull(whereClause.children);
554 		//writeln("after convertIsNullIsNotNull\n" ~ whereClause.dump(0));
555 		convertUnaryPlusMinus(whereClause.children);
556 		//writeln("after convertUnaryPlusMinus\n" ~ whereClause.dump(0));
557 		foldBraces(whereClause.children);
558 		//writeln("after foldBraces\n" ~ whereClause.dump(0));
559 		foldOperators(whereClause.children);
560 		//writeln("after foldOperators\n" ~ whereClause.dump(0));
561 		dropBraces(whereClause.children);
562 		//writeln("after dropBraces\n" ~ whereClause.dump(0));
563 	}
564 	
565 	void foldBraces(ref Token[] items) {
566 		while (true) {
567 			if (items.length == 0)
568 				return;
569 			int lastOpen = -1;
570 			int firstClose = -1;
571 			for (int i=0; i<items.length; i++) {
572 				if (items[i].type == TokenType.OpenBracket) {
573 					lastOpen = i;
574 				} if (items[i].type == TokenType.CloseBracket) {
575 					firstClose = i;
576 					break;
577 				}
578 			}
579 			if (lastOpen == -1 && firstClose == -1)
580 				return;
581 			//writeln("folding braces " ~ to!string(lastOpen) ~ " .. " ~ to!string(firstClose));
582 			enforceHelper!QuerySyntaxException(lastOpen >= 0 && lastOpen < firstClose, "Unpaired braces in WHERE clause" ~ errorContext(tokens[lastOpen]));
583 			Token folded = new Token(items[lastOpen].pos, TokenType.Braces, items, lastOpen + 1, firstClose);
584 			//			size_t oldlen = items.length;
585 			//			int removed = firstClose - lastOpen;
586 			replaceInPlace(items, lastOpen, firstClose + 1, [folded]);
587 			//			assert(items.length == oldlen - removed);
588 			foldBraces(folded.children);
589 		}
590 	}
591 	
592 	static void dropBraces(ref Token[] items) {
593 		foreach (t; items) {
594 			if (t.children.length > 0)
595 				dropBraces(t.children);
596 		}
597 		for (int i=0; i<items.length; i++) {
598 			if (items[i].type != TokenType.Braces)
599 				continue;
600 			if (items[i].children.length == 1) {
601 				Token t = items[i].children[0];
602 				replaceInPlace(items, i, i + 1, [t]);
603 			}
604 		}
605 	}
606 	
607 	void convertIsNullIsNotNull(ref Token[] items) {
608 		for (int i = cast(int)items.length - 2; i >= 0; i--) {
609 			if (items[i].type != TokenType.Operator || items[i + 1].type != TokenType.Keyword)
610 				continue;
611 			if (items[i].operator == OperatorType.IS && items[i + 1].keyword == KeywordType.NULL) {
612 				Token folded = new Token(items[i].pos,OperatorType.IS_NULL, "IS NULL");
613 				replaceInPlace(items, i, i + 2, [folded]);
614 				i-=2;
615 			}
616 		}
617 		for (int i = cast(int)items.length - 3; i >= 0; i--) {
618 			if (items[i].type != TokenType.Operator || items[i + 1].type != TokenType.Operator || items[i + 2].type != TokenType.Keyword)
619 				continue;
620 			if (items[i].operator == OperatorType.IS && items[i + 1].operator == OperatorType.NOT && items[i + 2].keyword == KeywordType.NULL) {
621 				Token folded = new Token(items[i].pos, OperatorType.IS_NOT_NULL, "IS NOT NULL");
622 				replaceInPlace(items, i, i + 3, [folded]);
623 				i-=3;
624 			}
625 		}
626 	}
627 	
628 	void convertFields(ref Token[] items) {
629 		while(true) {
630 			int p = -1;
631 			for (int i=0; i<items.length; i++) {
632 				if (items[i].type != TokenType.Ident && items[i].type != TokenType.Alias)
633 					continue;
634 				p = i;
635 				break;
636 			}
637 			if (p == -1)
638 				return;
639 			// found identifier at position p
640 			string[] idents;
641 			int lastp = p;
642 			idents ~= items[p].text;
643 			for (int i=p + 1; i < items.length - 1; i+=2) {
644 				if (items[i].type != TokenType.Dot)
645 					break;
646 				enforceHelper!QuerySyntaxException(i < items.length - 1 && items[i + 1].type == TokenType.Ident, "Syntax error in WHERE condition - no property name after . " ~ errorContext(items[p]));
647 				lastp = i + 1;
648 				idents ~= items[i + 1].text;
649 			}
650 			string fullName;
651 			FromClauseItem a;
652 			if (items[p].type == TokenType.Alias) {
653 				a = findFromClauseByAlias(idents[0]);
654 				idents.popFront();
655 			} else {
656 				// use first FROM clause if alias is not specified
657 				a = fromClause.first;
658 			}
659 			string aliasName = a.entityAlias;
660             EntityInfo ei = cast(EntityInfo)a.entity;
661 			enforceHelper!QuerySyntaxException(idents.length > 0, "Syntax error in WHERE condition - alias w/o property name: " ~ aliasName ~ errorContext(items[p]));
662             PropertyInfo pi;
663             fullName = aliasName;
664             while(true) {
665     			string propertyName = idents[0];
666     			idents.popFront();
667     			fullName ~= "." ~ propertyName;
668                 pi = cast(PropertyInfo)ei.findProperty(propertyName);
669     			while (pi.embedded) { // loop to allow nested @Embedded
670     				enforceHelper!QuerySyntaxException(idents.length > 0, "Syntax error in WHERE condition - @Embedded property reference should include reference to @Embeddable property " ~ aliasName ~ errorContext(items[p]));
671     				propertyName = idents[0];
672     				idents.popFront();
673                     pi = cast(PropertyInfo)pi.referencedEntity.findProperty(propertyName);
674     				fullName = fullName ~ "." ~ propertyName;
675     			}
676                 if (idents.length == 0)
677                     break;
678                 if (idents.length > 0) {
679                     // more field names
680                     string pname = idents[0];
681                     enforceHelper!QuerySyntaxException(pi.referencedEntity !is null, "Unexpected extra field name " ~ pname ~ " - property " ~ propertyName ~ " doesn't content subproperties " ~ errorContext(items[p]));
682                     ei = cast(EntityInfo)pi.referencedEntity;
683                     FromClauseItem newClause = fromClause.findByPath(fullName);
684                     if (newClause is null) {
685                         // autogenerate FROM clause
686                         newClause = fromClause.add(ei, null, pi.nullable ? JoinType.LeftJoin : JoinType.InnerJoin, false, a, pi);
687                     }
688                     a = newClause;
689                 }
690             }
691 			enforceHelper!QuerySyntaxException(idents.length == 0, "Unexpected extra field name " ~ idents[0] ~ errorContext(items[p]));
692 			//writeln("full name = " ~ fullName);
693 			Token t = new Token(items[p].pos, TokenType.Field, fullName);
694             t.entity = cast(EntityInfo)ei;
695             t.field = cast(PropertyInfo)pi;
696 			t.from = a;
697 			replaceInPlace(items, p, lastp + 1, [t]);
698 		}
699 	}
700 	
701 	static void convertUnaryPlusMinus(ref Token[] items) {
702 		foreach (t; items) {
703 			if (t.children.length > 0)
704 				convertUnaryPlusMinus(t.children);
705 		}
706 		for (int i=0; i<items.length; i++) {
707 			if (items[i].type != TokenType.Operator)
708 				continue;
709 			OperatorType op = items[i].operator;
710 			if (op == OperatorType.ADD || op == OperatorType.SUB) {
711 				// convert + and - to unary form if necessary
712 				if (i == 0 || !items[i - 1].isExpression()) {
713 					items[i].operator = (op == OperatorType.ADD) ? OperatorType.UNARY_PLUS : OperatorType.UNARY_MINUS;
714 				}
715 			}
716 		}
717 	}
718 	
719 	string errorContext(Token token) {
720 		return " near `" ~ query[token.pos .. $] ~ "` in query `" ~ query ~ "`";
721 	}
722 
723     void foldCommaSeparatedList(Token braces) {
724         // fold inside braces
725         Token[] items = braces.children;
726         int start = 0;
727         Token[] list;
728         for (int i=0; i <= items.length; i++) {
729             if (i == items.length || items[i].type == TokenType.Comma) {
730                 enforceHelper!QuerySyntaxException(i > start, "Empty item in comma separated list" ~ errorContext(items[i]));
731                 enforceHelper!QuerySyntaxException(i != items.length - 1, "Empty item in comma separated list" ~ errorContext(items[i]));
732                 Token item = new Token(items[start].pos, TokenType.Expression, braces.children, start, i);
733                 foldOperators(item.children);
734                 enforceHelper!QuerySyntaxException(item.children.length == 1, "Invalid expression in list item" ~ errorContext(items[i]));
735                 list ~= item.children[0];
736                 start = i + 1;
737             }
738         }
739         enforceHelper!QuerySyntaxException(list.length > 0, "Empty list" ~ errorContext(items[0]));
740         braces.type = TokenType.CommaDelimitedList;
741         braces.children = list;
742     }
743 
744 	void foldOperators(ref Token[] items) {
745 		foreach (t; items) {
746 			if (t.children.length > 0)
747 				foldOperators(t.children);
748 		}
749 		while (true) {
750 			//
751 			int bestOpPosition = -1;
752 			int bestOpPrecedency = -1;
753 			OperatorType t = OperatorType.NONE;
754 			for (int i=0; i<items.length; i++) {
755 				if (items[i].type != TokenType.Operator)
756 					continue;
757 				int p = operatorPrecedency(items[i].operator);
758 				if (p > bestOpPrecedency) {
759 					bestOpPrecedency = p;
760 					bestOpPosition = i;
761 					t = items[i].operator;
762 				}
763 			}
764 			if (bestOpPrecedency == -1)
765 				return;
766 			//writeln("Found op " ~ items[bestOpPosition].toString() ~ " at position " ~ to!string(bestOpPosition) ~ " with priority " ~ to!string(bestOpPrecedency));
767 			if (t == OperatorType.NOT || t == OperatorType.UNARY_PLUS || t == OperatorType.UNARY_MINUS) {
768 				// fold unary
769 				enforceHelper!QuerySyntaxException(bestOpPosition < items.length && items[bestOpPosition + 1].isExpression(), "Syntax error in WHERE condition " ~ errorContext(items[bestOpPosition]));
770 				Token folded = new Token(items[bestOpPosition].pos, t, items[bestOpPosition].text, items[bestOpPosition + 1]);
771 				replaceInPlace(items, bestOpPosition, bestOpPosition + 2, [folded]);
772 			} else if (t == OperatorType.IS_NULL || t == OperatorType.IS_NOT_NULL) {
773 				// fold unary
774 				enforceHelper!QuerySyntaxException(bestOpPosition > 0 && items[bestOpPosition - 1].isExpression(), "Syntax error in WHERE condition " ~ errorContext(items[bestOpPosition]));
775 				Token folded = new Token(items[bestOpPosition - 1].pos, t, items[bestOpPosition].text, items[bestOpPosition - 1]);
776 				replaceInPlace(items, bestOpPosition - 1, bestOpPosition + 1, [folded]);
777 			} else if (t == OperatorType.BETWEEN) {
778 				// fold  X BETWEEN A AND B
779 				enforceHelper!QuerySyntaxException(bestOpPosition > 0, "Syntax error in WHERE condition - no left arg for BETWEEN operator");
780 				enforceHelper!QuerySyntaxException(bestOpPosition < items.length - 1, "Syntax error in WHERE condition - no min bound for BETWEEN operator " ~ errorContext(items[bestOpPosition]));
781 				enforceHelper!QuerySyntaxException(bestOpPosition < items.length - 3, "Syntax error in WHERE condition - no max bound for BETWEEN operator " ~ errorContext(items[bestOpPosition]));
782 				enforceHelper!QuerySyntaxException(items[bestOpPosition + 2].operator == OperatorType.AND, "Syntax error in WHERE condition - no max bound for BETWEEN operator" ~ errorContext(items[bestOpPosition]));
783 				Token folded = new Token(items[bestOpPosition - 1].pos, t, items[bestOpPosition].text, items[bestOpPosition - 1]);
784 				folded.children ~= items[bestOpPosition + 1];
785 				folded.children ~= items[bestOpPosition + 3];
786 				replaceInPlace(items, bestOpPosition - 1, bestOpPosition + 4, [folded]);
787             } else if (t == OperatorType.IN) {
788                 // fold  X IN (A, B, ...)
789                 enforceHelper!QuerySyntaxException(bestOpPosition > 0, "Syntax error in WHERE condition - no left arg for IN operator");
790                 enforceHelper!QuerySyntaxException(bestOpPosition < items.length - 1, "Syntax error in WHERE condition - no value list for IN operator " ~ errorContext(items[bestOpPosition]));
791                 enforceHelper!QuerySyntaxException(items[bestOpPosition + 1].type == TokenType.Braces, "Syntax error in WHERE condition - no value list in braces for IN operator" ~ errorContext(items[bestOpPosition]));
792                 Token folded = new Token(items[bestOpPosition - 1].pos, t, items[bestOpPosition].text, items[bestOpPosition - 1]);
793                 folded.children ~= items[bestOpPosition + 1];
794                 foldCommaSeparatedList(items[bestOpPosition + 1]);
795                 replaceInPlace(items, bestOpPosition - 1, bestOpPosition + 2, [folded]);
796                 // fold value list
797                 //writeln("IN operator found: " ~ folded.dump(3));
798             } else {
799 				// fold binary
800 				enforceHelper!QuerySyntaxException(bestOpPosition > 0, "Syntax error in WHERE condition - no left arg for binary operator " ~ errorContext(items[bestOpPosition]));
801 				enforceHelper!QuerySyntaxException(bestOpPosition < items.length - 1, "Syntax error in WHERE condition - no right arg for binary operator " ~ errorContext(items[bestOpPosition]));
802 				//writeln("binary op " ~ items[bestOpPosition - 1].toString() ~ " " ~ items[bestOpPosition].toString() ~ " " ~ items[bestOpPosition + 1].toString());
803 				enforceHelper!QuerySyntaxException(items[bestOpPosition - 1].isExpression(), "Syntax error in WHERE condition - wrong type of left arg for binary operator " ~ errorContext(items[bestOpPosition]));
804 				enforceHelper!QuerySyntaxException(items[bestOpPosition + 1].isExpression(), "Syntax error in WHERE condition - wrong type of right arg for binary operator " ~ errorContext(items[bestOpPosition]));
805 				Token folded = new Token(items[bestOpPosition - 1].pos, t, items[bestOpPosition].text, items[bestOpPosition - 1], items[bestOpPosition + 1]);
806 				auto oldlen = items.length;
807 				replaceInPlace(items, bestOpPosition - 1, bestOpPosition + 2, [folded]);
808 				assert(items.length == oldlen - 2);
809 			}
810 		}
811 	}
812 	
813 	void parseOrderClause(int start, int end) {
814 		enforceHelper!QuerySyntaxException(start < end, "Invalid ORDER BY clause" ~ errorContext(tokens[start]));
815 		splitCommaDelimitedList(start, end, &parseOrderByClauseItem);
816 	}
817 	
818 	/// returns position of keyword in tokens array, -1 if not found
819 	int findKeyword(KeywordType k, int startFrom = 0) {
820 		for (int i = startFrom; i < tokens.length; i++) {
821 			if (tokens[i].type == TokenType.Keyword && tokens[i].keyword == k)
822 				return i;
823 		}
824 		return -1;
825 	}
826 	
827     int addSelectSQL(Dialect dialect, ParsedQuery res, string tableName, bool first, const EntityInfo ei) {
828         int colCount = 0;
829         for(int j = 0; j < ei.getPropertyCount(); j++) {
830             PropertyInfo f = cast(PropertyInfo)ei.getProperty(j);
831             string fieldName = f.columnName;
832             if (f.embedded) {
833                 // put embedded cols here
834                 colCount += addSelectSQL(dialect, res, tableName, first && colCount == 0, f.referencedEntity);
835                 continue;
836             } else if (f.oneToOne) {
837             } else {
838             }
839             if (fieldName is null)
840                 continue;
841             if (!first || colCount > 0) {
842                 res.appendSQL(", ");
843             } else
844                 first = false;
845             
846             res.appendSQL(tableName ~ "." ~ dialect.quoteIfNeeded(fieldName));
847             colCount++;
848         }
849         return colCount;
850     }
851 
852 	void addSelectSQL(Dialect dialect, ParsedQuery res) {
853 		res.appendSQL("SELECT ");
854 		bool first = true;
855 		assert(selectClause.length > 0);
856 		int colCount = 0;
857         foreach(i, s; selectClause) {
858             s.from.selectIndex = cast(int)i;
859         }
860 		if (selectClause[0].prop is null) {
861 			// object alias is specified: add all properties of object
862             //writeln("selected entity count: " ~ to!string(selectClause.length));
863             res.setEntity(selectClause[0].from.entity);
864             for(int i = 0; i < fromClause.length; i++) {
865                 FromClauseItem from = fromClause[i];
866                 if (!from.fetch)
867                     continue;
868                 string tableName = from.sqlAlias;
869     			assert(from !is null);
870     			assert(from.entity !is null);
871                 colCount += addSelectSQL(dialect, res, tableName, colCount == 0, from.entity);
872             }
873 		} else {
874 			// individual fields specified
875 			res.setEntity(null);
876 			foreach(a; selectClause) {
877 				string fieldName = a.prop.columnName;
878 				string tableName = a.from.sqlAlias;
879 				if (!first) {
880 					res.appendSQL(", ");
881 				} else
882 					first = false;
883 				res.appendSQL(tableName ~ "." ~ dialect.quoteIfNeeded(fieldName));
884 				colCount++;
885 			}
886 		}
887 		res.setColCount(colCount);
888         res.setSelect(selectClause);
889 	}
890 	
891 	void addFromSQL(Dialect dialect, ParsedQuery res) {
892         res.setFromClause(fromClause);
893 		res.appendSpace();
894 		res.appendSQL("FROM ");
895 		res.appendSQL(dialect.quoteIfNeeded(fromClause.first.entity.tableName) ~ " AS " ~ fromClause.first.sqlAlias);
896         for (int i = 1; i < fromClause.length; i++) {
897             FromClauseItem join = fromClause[i];
898             FromClauseItem base = join.base;
899             assert(join !is null && base !is null);
900             res.appendSpace();
901 
902             assert(join.baseProperty !is null);
903             if (join.baseProperty.manyToMany) {
904                 string joinTableAlias = base.sqlAlias ~ join.sqlAlias;
905                 res.appendSQL(join.joinType == JoinType.LeftJoin ? "LEFT JOIN " : "INNER JOIN ");
906 
907                 res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.joinTable.tableName) ~ " AS " ~ joinTableAlias);
908                 res.appendSQL(" ON ");
909                 res.appendSQL(base.sqlAlias);
910                 res.appendSQL(".");
911                 res.appendSQL(dialect.quoteIfNeeded(base.entity.getKeyProperty().columnName));
912                 res.appendSQL("=");
913                 res.appendSQL(joinTableAlias);
914                 res.appendSQL(".");
915                 res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.joinTable.column1));
916 
917                 res.appendSpace();
918 
919                 res.appendSQL(join.joinType == JoinType.LeftJoin ? "LEFT JOIN " : "INNER JOIN ");
920                 res.appendSQL(dialect.quoteIfNeeded(join.entity.tableName) ~ " AS " ~ join.sqlAlias);
921                 res.appendSQL(" ON ");
922                 res.appendSQL(joinTableAlias);
923                 res.appendSQL(".");
924                 res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.joinTable.column2));
925                 res.appendSQL("=");
926                 res.appendSQL(join.sqlAlias);
927                 res.appendSQL(".");
928                 res.appendSQL(dialect.quoteIfNeeded(join.entity.getKeyProperty().columnName));
929             } else {
930                 res.appendSQL(join.joinType == JoinType.LeftJoin ? "LEFT JOIN " : "INNER JOIN ");
931                 res.appendSQL(dialect.quoteIfNeeded(join.entity.tableName) ~ " AS " ~ join.sqlAlias);
932                 res.appendSQL(" ON ");
933                 //writeln("adding ON");
934                 if (join.baseProperty.oneToOne) {
935                     assert(join.baseProperty.columnName !is null || join.baseProperty.referencedProperty !is null);
936                     if (join.baseProperty.columnName !is null) {
937                         //writeln("fk is in base");
938                         res.appendSQL(base.sqlAlias);
939                         res.appendSQL(".");
940                         res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.columnName));
941                         res.appendSQL("=");
942                         res.appendSQL(join.sqlAlias);
943                         res.appendSQL(".");
944                         res.appendSQL(dialect.quoteIfNeeded(join.entity.getKeyProperty().columnName));
945                     } else {
946                         //writeln("fk is in join");
947                         res.appendSQL(base.sqlAlias);
948                         res.appendSQL(".");
949                         res.appendSQL(dialect.quoteIfNeeded(base.entity.getKeyProperty().columnName));
950                         res.appendSQL("=");
951                         res.appendSQL(join.sqlAlias);
952                         res.appendSQL(".");
953                         res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.referencedProperty.columnName));
954                     }
955                 } else if (join.baseProperty.manyToOne) {
956                     assert(join.baseProperty.columnName !is null, "ManyToOne should have JoinColumn as well");
957                     //writeln("fk is in base");
958                     res.appendSQL(base.sqlAlias);
959                     res.appendSQL(".");
960                     res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.columnName));
961                     res.appendSQL("=");
962                     res.appendSQL(join.sqlAlias);
963                     res.appendSQL(".");
964                     res.appendSQL(dialect.quoteIfNeeded(join.entity.getKeyProperty().columnName));
965                 } else if (join.baseProperty.oneToMany) {
966                     res.appendSQL(base.sqlAlias);
967                     res.appendSQL(".");
968                     res.appendSQL(dialect.quoteIfNeeded(base.entity.getKeyProperty().columnName));
969                     res.appendSQL("=");
970                     res.appendSQL(join.sqlAlias);
971                     res.appendSQL(".");
972                     res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.referencedProperty.columnName));
973                 } else {
974                     // TODO: support other relations
975                     throw new QuerySyntaxException("Invalid relation type in join");
976                 }
977             }
978         }
979 	}
980 	
981 	void addWhereCondition(Token t, int basePrecedency, Dialect dialect, ParsedQuery res) {
982 		if (t.type == TokenType.Expression) {
983 			addWhereCondition(t.children[0], basePrecedency, dialect, res);
984 		} else if (t.type == TokenType.Field) {
985 			string tableName = t.from.sqlAlias;
986 			string fieldName = t.field.columnName;
987 			res.appendSpace();
988 			res.appendSQL(tableName ~ "." ~ dialect.quoteIfNeeded(fieldName));
989 		} else if (t.type == TokenType.Number) {
990 			res.appendSpace();
991 			res.appendSQL(t.text);
992 		} else if (t.type == TokenType.String) {
993 			res.appendSpace();
994 			res.appendSQL(dialect.quoteSqlString(t.text));
995 		} else if (t.type == TokenType.Parameter) {
996 			res.appendSpace();
997 			res.appendSQL("?");
998 			res.addParam(t.text);
999         } else if (t.type == TokenType.CommaDelimitedList) {
1000             bool first = true;
1001             for (int i=0; i<t.children.length; i++) {
1002                 if (!first)
1003                     res.appendSQL(", ");
1004                 else
1005                     first = false;
1006                 addWhereCondition(t.children[i], 0, dialect, res);
1007             }
1008         } else if (t.type == TokenType.OpExpr) {
1009 			int currentPrecedency = operatorPrecedency(t.operator);
1010 			bool needBraces = currentPrecedency < basePrecedency;
1011 			if (needBraces)
1012 				res.appendSQL("(");
1013 			switch(t.operator) {
1014 				case OperatorType.LIKE:
1015 				case OperatorType.EQ:
1016 				case OperatorType.NE:
1017 				case OperatorType.LT:
1018 				case OperatorType.GT:
1019 				case OperatorType.LE:
1020 				case OperatorType.GE:
1021 				case OperatorType.MUL:
1022 				case OperatorType.ADD:
1023 				case OperatorType.SUB:
1024 				case OperatorType.DIV:
1025 				case OperatorType.AND:
1026 				case OperatorType.OR:
1027 				case OperatorType.IDIV:
1028 				case OperatorType.MOD:
1029 					// binary op
1030 					if (!needBraces)
1031 						res.appendSpace();
1032 					addWhereCondition(t.children[0], currentPrecedency, dialect, res);
1033 					res.appendSpace();
1034 					res.appendSQL(t.text);
1035 					res.appendSpace();
1036 					addWhereCondition(t.children[1], currentPrecedency, dialect, res);
1037 					break;
1038 				case OperatorType.UNARY_PLUS:
1039 				case OperatorType.UNARY_MINUS:
1040 				case OperatorType.NOT:
1041 					// unary op
1042 					if (!needBraces)
1043 						res.appendSpace();
1044 					res.appendSQL(t.text);
1045 					res.appendSpace();
1046 					addWhereCondition(t.children[0], currentPrecedency, dialect, res);
1047 					break;
1048 				case OperatorType.IS_NULL:
1049 				case OperatorType.IS_NOT_NULL:
1050 					addWhereCondition(t.children[0], currentPrecedency, dialect, res);
1051 					res.appendSpace();
1052 					res.appendSQL(t.text);
1053 					res.appendSpace();
1054 					break;
1055 				case OperatorType.BETWEEN:
1056 					if (!needBraces)
1057 						res.appendSpace();
1058 					addWhereCondition(t.children[0], currentPrecedency, dialect, res);
1059 					res.appendSQL(" BETWEEN ");
1060 					addWhereCondition(t.children[1], currentPrecedency, dialect, res);
1061 					res.appendSQL(" AND ");
1062 					addWhereCondition(t.children[2], currentPrecedency, dialect, res);
1063 					break;
1064                 case OperatorType.IN:
1065                     if (!needBraces)
1066                         res.appendSpace();
1067                     addWhereCondition(t.children[0], currentPrecedency, dialect, res);
1068                     res.appendSQL(" IN (");
1069                     addWhereCondition(t.children[1], currentPrecedency, dialect, res);
1070                     res.appendSQL(")");
1071                     break;
1072 				case OperatorType.IS:
1073 				default:
1074 					enforceHelper!QuerySyntaxException(false, "Unexpected operator" ~ errorContext(t));
1075 					break;
1076 			}
1077 			if (needBraces)
1078 				res.appendSQL(")");
1079 		}
1080 	}
1081 	
1082 	void addWhereSQL(Dialect dialect, ParsedQuery res) {
1083 		if (whereClause is null)
1084 			return;
1085 		res.appendSpace();
1086 		res.appendSQL("WHERE ");
1087 		addWhereCondition(whereClause, 0, dialect, res);
1088 	}
1089 	
1090 	void addOrderBySQL(Dialect dialect, ParsedQuery res) {
1091 		if (orderByClause.length == 0)
1092 			return;
1093 		res.appendSpace();
1094 		res.appendSQL("ORDER BY ");
1095 		bool first = true;
1096 		// individual fields specified
1097 		foreach(a; orderByClause) {
1098 			string fieldName = a.prop.columnName;
1099 			string tableName = a.from.sqlAlias;
1100 			if (!first) {
1101 				res.appendSQL(", ");
1102 			} else 
1103 				first = false;
1104 			res.appendSQL(tableName ~ "." ~ dialect.quoteIfNeeded(fieldName));
1105 			if (!a.asc)
1106 				res.appendSQL(" DESC");
1107 		}
1108 	}
1109 	
1110 	ParsedQuery makeSQL(Dialect dialect) {
1111 		ParsedQuery res = new ParsedQuery(query);
1112 		addSelectSQL(dialect, res);
1113 		addFromSQL(dialect, res);
1114 		addWhereSQL(dialect, res);
1115 		addOrderBySQL(dialect, res);
1116 		return res;
1117 	}
1118 	
1119 }
1120 
1121 enum KeywordType {
1122 	NONE,
1123 	SELECT,
1124 	FROM,
1125 	WHERE,
1126 	ORDER,
1127 	BY,
1128 	ASC,
1129 	DESC,
1130 	JOIN,
1131 	INNER,
1132 	OUTER,
1133 	LEFT,
1134 	RIGHT,
1135     FETCH,
1136     AS,
1137 	LIKE,
1138 	IN,
1139 	IS,
1140 	NOT,
1141 	NULL,
1142 	AND,
1143 	OR,
1144 	BETWEEN,
1145 	DIV,
1146 	MOD,
1147 }
1148 
1149 KeywordType isKeyword(string str) {
1150 	return isKeyword(str.dup);
1151 }
1152 
1153 KeywordType isKeyword(char[] str) {
1154 	char[] s = toUpper(str);
1155 	if (s=="SELECT") return KeywordType.SELECT;
1156 	if (s=="FROM") return KeywordType.FROM;
1157 	if (s=="WHERE") return KeywordType.WHERE;
1158 	if (s=="ORDER") return KeywordType.ORDER;
1159 	if (s=="BY") return KeywordType.BY;
1160 	if (s=="ASC") return KeywordType.ASC;
1161 	if (s=="DESC") return KeywordType.DESC;
1162 	if (s=="JOIN") return KeywordType.JOIN;
1163 	if (s=="INNER") return KeywordType.INNER;
1164 	if (s=="OUTER") return KeywordType.OUTER;
1165 	if (s=="LEFT") return KeywordType.LEFT;
1166 	if (s=="RIGHT") return KeywordType.RIGHT;
1167     if (s=="FETCH") return KeywordType.FETCH;
1168     if (s=="LIKE") return KeywordType.LIKE;
1169 	if (s=="IN") return KeywordType.IN;
1170 	if (s=="IS") return KeywordType.IS;
1171 	if (s=="NOT") return KeywordType.NOT;
1172 	if (s=="NULL") return KeywordType.NULL;
1173 	if (s=="AS") return KeywordType.AS;
1174 	if (s=="AND") return KeywordType.AND;
1175 	if (s=="OR") return KeywordType.OR;
1176 	if (s=="BETWEEN") return KeywordType.BETWEEN;
1177 	if (s=="DIV") return KeywordType.DIV;
1178 	if (s=="MOD") return KeywordType.MOD;
1179 	return KeywordType.NONE;
1180 }
1181 
1182 unittest {
1183 	assert(isKeyword("Null") == KeywordType.NULL);
1184 	assert(isKeyword("from") == KeywordType.FROM);
1185 	assert(isKeyword("SELECT") == KeywordType.SELECT);
1186 	assert(isKeyword("blabla") == KeywordType.NONE);
1187 }
1188 
1189 enum OperatorType {
1190 	NONE,
1191 	
1192 	// symbolic
1193 	EQ, // ==
1194 	NE, // != <>
1195 	LT, // <
1196 	GT, // >
1197 	LE, // <=
1198 	GE, // >=
1199 	MUL,// *
1200 	ADD,// +
1201 	SUB,// -
1202 	DIV,// /
1203 	
1204 	// from keywords
1205 	LIKE,
1206 	IN,
1207 	IS,
1208 	NOT,
1209 	AND,
1210 	OR,
1211 	BETWEEN,
1212 	IDIV,
1213 	MOD,
1214 	
1215 	UNARY_PLUS,
1216 	UNARY_MINUS,
1217 	
1218 	IS_NULL,
1219 	IS_NOT_NULL,
1220 }
1221 
1222 OperatorType isOperator(KeywordType t) {
1223 	switch (t) {
1224 		case KeywordType.LIKE: return OperatorType.LIKE;
1225 		case KeywordType.IN: return OperatorType.IN;
1226 		case KeywordType.IS: return OperatorType.IS;
1227 		case KeywordType.NOT: return OperatorType.NOT;
1228 		case KeywordType.AND: return OperatorType.AND;
1229 		case KeywordType.OR: return OperatorType.OR;
1230 		case KeywordType.BETWEEN: return OperatorType.BETWEEN;
1231 		case KeywordType.DIV: return OperatorType.IDIV;
1232 		case KeywordType.MOD: return OperatorType.MOD;
1233 		default: return OperatorType.NONE;
1234 	}
1235 }
1236 
1237 int operatorPrecedency(OperatorType t) {
1238 	switch(t) {
1239 		case OperatorType.EQ: return 5; // ==
1240 		case OperatorType.NE: return 5; // != <>
1241 		case OperatorType.LT: return 5; // <
1242 		case OperatorType.GT: return 5; // >
1243 		case OperatorType.LE: return 5; // <=
1244 		case OperatorType.GE: return 5; // >=
1245 		case OperatorType.MUL: return 10; // *
1246 		case OperatorType.ADD: return 9; // +
1247 		case OperatorType.SUB: return 9; // -
1248 		case OperatorType.DIV: return 10; // /
1249 			// from keywords
1250 		case OperatorType.LIKE: return 11;
1251 		case OperatorType.IN: return 12;
1252 		case OperatorType.IS: return 13;
1253 		case OperatorType.NOT: return 6; // ???
1254 		case OperatorType.AND: return 4;
1255 		case OperatorType.OR:  return 3;
1256 		case OperatorType.BETWEEN: return 7; // ???
1257 		case OperatorType.IDIV: return 10;
1258 		case OperatorType.MOD: return 10;
1259 		case OperatorType.UNARY_PLUS: return 15;
1260 		case OperatorType.UNARY_MINUS: return 15;
1261 		case OperatorType.IS_NULL: return 15;
1262 		case OperatorType.IS_NOT_NULL: return 15;
1263 		default: return -1;
1264 	}
1265 }
1266 
1267 OperatorType isOperator(string s, ref int i) {
1268 	int len = cast(int)s.length;
1269 	char ch = s[i];
1270 	char ch2 = i < len - 1 ? s[i + 1] : 0;
1271 	//char ch3 = i < len - 2 ? s[i + 2] : 0;
1272 	if (ch == '=' && ch2 == '=') { i++; return OperatorType.EQ; } // ==
1273 	if (ch == '!' && ch2 == '=') { i++; return OperatorType.NE; } // !=
1274 	if (ch == '<' && ch2 == '>') { i++; return OperatorType.NE; } // <>
1275 	if (ch == '<' && ch2 == '=') { i++; return OperatorType.LE; } // <=
1276 	if (ch == '>' && ch2 == '=') { i++; return OperatorType.GE; } // >=
1277 	if (ch == '=') return OperatorType.EQ; // =
1278 	if (ch == '<') return OperatorType.LT; // <
1279 	if (ch == '>') return OperatorType.GT; // <
1280 	if (ch == '*') return OperatorType.MUL; // <
1281 	if (ch == '+') return OperatorType.ADD; // <
1282 	if (ch == '-') return OperatorType.SUB; // <
1283 	if (ch == '/') return OperatorType.DIV; // <
1284 	return OperatorType.NONE;
1285 }
1286 
1287 
1288 enum TokenType {
1289 	Keyword,      // WHERE
1290 	Ident,        // ident
1291 	Number,       // 25   13.5e-10
1292 	String,       // 'string'
1293 	Operator,     // == != <= >= < > + - * /
1294 	Dot,          // .
1295 	OpenBracket,  // (
1296 	CloseBracket, // )
1297 	Comma,        // ,
1298 	Entity,       // entity name
1299 	Field,        // field name of some entity
1300 	Alias,        // alias name of some entity
1301 	Parameter,    // ident after :
1302 	// types of compound AST nodes
1303 	Expression,   // any expression
1304 	Braces,       // ( tokens )
1305 	CommaDelimitedList, // tokens, ... , tokens
1306 	OpExpr, // operator expression; current token == operator, children = params
1307 }
1308 
1309 class Token {
1310 	int pos;
1311 	TokenType type;
1312 	KeywordType keyword = KeywordType.NONE;
1313 	OperatorType operator = OperatorType.NONE;
1314 	string text;
1315 	string spaceAfter;
1316 	EntityInfo entity;
1317     PropertyInfo field;
1318 	FromClauseItem from;
1319 	Token[] children;
1320 	this(int pos, TokenType type, string text) {
1321 		this.pos = pos;
1322 		this.type = type;
1323 		this.text = text;
1324 	}
1325 	this(int pos, KeywordType keyword, string text) {
1326 		this.pos = pos;
1327 		this.type = TokenType.Keyword;
1328 		this.keyword = keyword;
1329 		this.text = text;
1330 	}
1331 	this(int pos, OperatorType op, string text) {
1332 		this.pos = pos;
1333 		this.type = TokenType.Operator;
1334 		this.operator = op;
1335 		this.text = text;
1336 	}
1337 	this(int pos, TokenType type, Token[] base, int start, int end) {
1338 		this.pos = pos;
1339 		this.type = type;
1340 		this.children = new Token[end - start];
1341 		for (int i = start; i < end; i++)
1342 			children[i - start] = base[i];
1343 	}
1344 	// unary operator expression
1345 	this(int pos, OperatorType type, string text, Token right) {
1346 		this.pos = pos;
1347 		this.type = TokenType.OpExpr;
1348 		this.operator = type;
1349 		this.text = text;
1350 		this.children = new Token[1];
1351 		this.children[0] = right;
1352 	}
1353 	// binary operator expression
1354 	this(int pos, OperatorType type, string text, Token left, Token right) {
1355 		this.pos = pos;
1356 		this.type = TokenType.OpExpr;
1357 		this.text = text;
1358 		this.operator = type;
1359 		this.children = new Token[2];
1360 		this.children[0] = left;
1361 		this.children[1] = right;
1362 	}
1363 	bool isExpression() {
1364 		return type==TokenType.Expression || type==TokenType.Braces || type==TokenType.OpExpr || type==TokenType.Parameter 
1365 			|| type==TokenType.Field || type==TokenType.String || type==TokenType.Number;
1366 	}
1367 	bool isCompound() {
1368 		return this.type >= TokenType.Expression;
1369 	}
1370 	string dump(int level) {
1371 		string res;
1372 		for (int i=0; i<level; i++)
1373 			res ~= "    ";
1374 		res ~= toString() ~ "\n";
1375 		foreach (c; children)
1376 			res ~= c.dump(level + 1);
1377 		return res;
1378 	}
1379 	override string toString() {
1380 		switch (type) {
1381 			case TokenType.Keyword:      // WHERE
1382 			case TokenType.Ident: return "`" ~ text ~ "`";        // ident
1383 			case TokenType.Number: return "" ~ text;       // 25   13.5e-10
1384 			case TokenType.String: return "'" ~ text ~ "'";       // 'string'
1385 			case TokenType.Operator: return "op:" ~ text;     // == != <= >= < > + - * /
1386 			case TokenType.Dot: return ".";          // .
1387 			case TokenType.OpenBracket: return "(";  // (
1388 			case TokenType.CloseBracket: return ")"; // )
1389 			case TokenType.Comma: return ",";        // ,
1390 			case TokenType.Entity: return "entity: " ~ entity.name;       // entity name
1391 			case TokenType.Field: return from.entityAlias ~ "." ~ field.propertyName;        // field name of some entity
1392 			case TokenType.Alias: return "alias: " ~ text;        // alias name of some entity
1393 			case TokenType.Parameter: return ":" ~ text;    // ident after :
1394 				// types of compound AST nodes
1395 			case TokenType.Expression: return "expr";   // any expression
1396 			case TokenType.Braces: return "()";       // ( tokens )
1397 			case TokenType.CommaDelimitedList: return ",,,"; // tokens, ... , tokens
1398 			case TokenType.OpExpr: return "" ~ text;
1399 			default: return "UNKNOWN";
1400 		}
1401 	}
1402 	
1403 }
1404 
1405 Token[] tokenize(string s) {
1406 	Token[] res;
1407 	int startpos = 0;
1408 	int state = 0;
1409 	int len = cast(int)s.length;
1410 	for (int i=0; i<len; i++) {
1411 		char ch = s[i];
1412 		char ch2 = i < len - 1 ? s[i + 1] : 0;
1413 		char ch3 = i < len - 2 ? s[i + 2] : 0;
1414 		string text;
1415 		bool quotedIdent = ch == '`';
1416 		startpos = i;
1417 		OperatorType op = isOperator(s, i);
1418 		if (op != OperatorType.NONE) {
1419 			// operator
1420 			res ~= new Token(startpos, op, s[startpos .. i + 1]);
1421 		} else if (ch == ':' && (isAlpha(ch2) || ch2=='_')) {
1422 			// parameter name
1423 			i++;
1424 			// && state == 0
1425 			for(int j=i; j<len; j++) {
1426 				if (isAlphaNum(s[j]) || s[j] == '_') {
1427 					text ~= s[j];
1428 					i = j;
1429 				} else {
1430 					break;
1431 				}
1432 			}
1433 			enforceHelper!QuerySyntaxException(text.length > 0, "Invalid parameter name near " ~ cast(string)s[startpos .. $]);
1434 			res ~= new Token(startpos, TokenType.Parameter, text);
1435 		} else if (isAlpha(ch) || ch=='_' || quotedIdent) {
1436 			// identifier or keyword
1437 			if (quotedIdent) {
1438 				i++;
1439 				enforceHelper!QuerySyntaxException(i < len - 1, "Invalid quoted identifier near " ~ cast(string)s[startpos .. $]);
1440 			}
1441 			// && state == 0
1442 			for(int j=i; j<len; j++) {
1443 				if (isAlphaNum(s[j]) || s[j] == '_') {
1444 					text ~= s[j];
1445 					i = j;
1446 				} else {
1447 					break;
1448 				}
1449 			}
1450 			enforceHelper!QuerySyntaxException(text.length > 0, "Invalid quoted identifier near " ~ cast(string)s[startpos .. $]);
1451 			if (quotedIdent) {
1452 				enforceHelper!QuerySyntaxException(i < len - 1 && s[i + 1] == '`', "Invalid quoted identifier near " ~ cast(string)s[startpos .. $]);
1453 				i++;
1454 			}
1455 			KeywordType keywordId = isKeyword(text);
1456 			if (keywordId != KeywordType.NONE && !quotedIdent) {
1457 				OperatorType keywordOp = isOperator(keywordId);
1458 				if (keywordOp != OperatorType.NONE)
1459 					res ~= new Token(startpos, keywordOp, text); // operator keyword
1460 				else
1461 					res ~= new Token(startpos, keywordId, text);
1462 			} else
1463 				res ~= new Token(startpos, TokenType.Ident, text);
1464 		} else if (isWhite(ch)) {
1465 			// whitespace
1466 			for(int j=i; j<len; j++) {
1467 				if (isWhite(s[j])) {
1468 					text ~= s[j];
1469 					i = j;
1470 				} else {
1471 					break;
1472 				}
1473 			}
1474 			// don't add whitespace to lexer results as separate token
1475 			// add as spaceAfter
1476 			if (res.length > 0) {
1477 				res[$ - 1].spaceAfter = text;
1478 			}
1479 		} else if (ch == '\'') {
1480 			// string constant
1481 			i++;
1482 			for(int j=i; j<len; j++) {
1483 				if (s[j] != '\'') {
1484 					text ~= s[j];
1485 					i = j;
1486 				} else {
1487 					break;
1488 				}
1489 			}
1490 			enforceHelper!QuerySyntaxException(i < len - 1 && s[i + 1] == '\'', "Unfinished string near " ~ cast(string)s[startpos .. $]);
1491 			i++;
1492 			res ~= new Token(startpos, TokenType.String, text);
1493 		} else if (isDigit(ch) || (ch == '.' && isDigit(ch2))) {
1494 			// numeric constant
1495 			if (ch == '.') {
1496 				// .25
1497 				text ~= '.';
1498 				i++;
1499 				for(int j = i; j<len; j++) {
1500 					if (isDigit(s[j])) {
1501 						text ~= s[j];
1502 						i = j;
1503 					} else {
1504 						break;
1505 					}
1506 				}
1507 			} else {
1508 				// 123
1509 				for(int j=i; j<len; j++) {
1510 					if (isDigit(s[j])) {
1511 						text ~= s[j];
1512 						i = j;
1513 					} else {
1514 						break;
1515 					}
1516 				}
1517 				// .25
1518 				if (i < len - 1 && s[i + 1] == '.') {
1519 					text ~= '.';
1520 					i++;
1521 					for(int j = i; j<len; j++) {
1522 						if (isDigit(s[j])) {
1523 							text ~= s[j];
1524 							i = j;
1525 						} else {
1526 							break;
1527 						}
1528 					}
1529 				}
1530 			}
1531 			if (i < len - 1 && std.ascii.toLower(s[i + 1]) == 'e') {
1532 				text ~= s[i+1];
1533 				i++;
1534 				if (i < len - 1 && (s[i + 1] == '-' || s[i + 1] == '+')) {
1535 					text ~= s[i+1];
1536 					i++;
1537 				}
1538 				enforceHelper!QuerySyntaxException(i < len - 1 && isDigit(s[i]), "Invalid number near " ~ cast(string)s[startpos .. $]);
1539 				for(int j = i; j<len; j++) {
1540 					if (isDigit(s[j])) {
1541 						text ~= s[j];
1542 						i = j;
1543 					} else {
1544 						break;
1545 					}
1546 				}
1547 			}
1548 			enforceHelper!QuerySyntaxException(i >= len - 1 || !isAlpha(s[i]), "Invalid number near " ~ cast(string)s[startpos .. $]);
1549 			res ~= new Token(startpos, TokenType.Number, text);
1550 		} else if (ch == '.') {
1551 			res ~= new Token(startpos, TokenType.Dot, ".");
1552 		} else if (ch == '(') {
1553 			res ~= new Token(startpos, TokenType.OpenBracket, "(");
1554 		} else if (ch == ')') {
1555 			res ~= new Token(startpos, TokenType.CloseBracket, ")");
1556 		} else if (ch == ',') {
1557 			res ~= new Token(startpos, TokenType.Comma, ",");
1558 		} else {
1559 			enforceHelper!QuerySyntaxException(false, "Invalid character near " ~ cast(string)s[startpos .. $]);
1560 		}
1561 	}
1562 	return res;
1563 }
1564 
1565 unittest {
1566 	Token[] tokens;
1567 	tokens = tokenize("SELECT a From User a where a.flags = 12 AND a.name='john' ORDER BY a.idx ASC");
1568 	assert(tokens.length == 23);
1569 	assert(tokens[0].type == TokenType.Keyword);
1570 	assert(tokens[2].type == TokenType.Keyword);
1571 	assert(tokens[5].type == TokenType.Keyword);
1572 	assert(tokens[5].text == "where");
1573 	assert(tokens[10].type == TokenType.Number);
1574 	assert(tokens[10].text == "12");
1575 	assert(tokens[16].type == TokenType.String);
1576 	assert(tokens[16].text == "john");
1577 	assert(tokens[22].type == TokenType.Keyword);
1578 	assert(tokens[22].text == "ASC");
1579 }
1580 
1581 class ParameterValues {
1582 	Variant[string] values;
1583 	int[][string]params;
1584 	int[string]unboundParams;
1585 	this(int[][string]params) {
1586 		this.params = params;
1587 		foreach(key, value; params) {
1588 			unboundParams[key] = 1;
1589 		}
1590 	}
1591 	void setParameter(string name, Variant value) {
1592         enforceHelper!QueryParameterException((name in params) !is null, "Attempting to set unknown parameter " ~ name);
1593 		unboundParams.remove(name);
1594 		values[name] = value;
1595 	}
1596 	void checkAllParametersSet() {
1597 		if (unboundParams.length == 0)
1598 			return;
1599 		string list;
1600 		foreach(key, value; unboundParams) {
1601 			if (list.length > 0)
1602 				list ~= ", ";
1603 			list ~= key;
1604 		}
1605         enforceHelper!QueryParameterException(false, "Parameters " ~ list ~ " not set");
1606 	}
1607 	void applyParams(DataSetWriter ds) {
1608 		foreach(key, indexes; params) {
1609 			Variant value = values[key];
1610 			foreach(i; indexes)
1611 				ds.setVariant(i, value);
1612 		}
1613 	}
1614 }
1615 
1616 class ParsedQuery {
1617 	private string _hql;
1618 	private string _sql;
1619 	private int[][string]params; // contains 1-based indexes of ? ? ? placeholders in SQL for param by name
1620 	private int paramIndex = 1;
1621     private FromClause _from;
1622     private SelectClauseItem[] _select;
1623     private EntityInfo _entity;
1624 	private int _colCount = 0;
1625 	this(string hql) {
1626 		_hql = hql;
1627 	}
1628 	@property string hql() { return _hql; }
1629 	@property string sql() { return _sql; }
1630 	@property const(EntityInfo)entity() { return _entity; }
1631 	@property int colCount() { return _colCount; }
1632     @property FromClause from() { return _from; }
1633     @property SelectClauseItem[] select() { return _select; }
1634     void setEntity(const EntityInfo entity) {
1635         _entity = cast(EntityInfo)entity;
1636 	}
1637     void setFromClause(FromClause from) {
1638         _from = from;
1639     }
1640     void setSelect(SelectClauseItem[] items) {
1641         _select = items; 
1642     }
1643     void setColCount(int cnt) { _colCount = cnt; }
1644 	void addParam(string paramName) {
1645 		if ((paramName in params) is null) {
1646 			params[paramName] = [paramIndex++];
1647 		} else {
1648 			params[paramName] ~= [paramIndex++];
1649 		}
1650 	}
1651 	int[] getParam(string paramName) {
1652 		if ((paramName in params) is null) {
1653 			throw new DStructException("Parameter " ~ paramName ~ " not found in query " ~ _hql);
1654 		} else {
1655 			return params[paramName];
1656 		}
1657 	}
1658 	void appendSQL(string sql) {
1659 		_sql ~= sql;
1660 	}
1661 	void appendSpace() {
1662 		if (_sql.length > 0 && _sql[$ - 1] != ' ')
1663 			_sql ~= ' ';
1664 	}
1665 	ParameterValues createParams() {
1666 		return new ParameterValues(params);
1667 	}
1668 }
1669 
1670 // unittest {
1671 // 	ParsedQuery q = new ParsedQuery("FROM User where id = :param1 or id = :param2");
1672 // 	q.addParam("param1"); // 1
1673 // 	q.addParam("param2"); // 2
1674 // 	q.addParam("param1"); // 3
1675 // 	q.addParam("param1"); // 4
1676 // 	q.addParam("param3"); // 5
1677 // 	q.addParam("param2"); // 6
1678 // 	assert(q.getParam("param1") == [1,3,4]);
1679 // 	assert(q.getParam("param2") == [2,6]);
1680 // 	assert(q.getParam("param3") == [5]);
1681 // }
1682 
1683 // unittest {
1684 //     import dstruct.tests;
1685 
1686 //     EntityMetaData schema = new SchemaInfoImpl!(User, Customer, AccountType, Address, Person, MoreInfo, EvenMoreInfo, Role);
1687 // 	QueryParser parser = new QueryParser(schema, "SELECT a FROM User AS a WHERE id = :Id AND name != :skipName OR name IS NULL  AND a.flags IS NOT NULL ORDER BY name, a.flags DESC");
1688 // 	assert(parser.parameterNames.length == 2);
1689 // 	//writeln("param1=" ~ parser.parameterNames[0]);
1690 // 	//writeln("param2=" ~ parser.parameterNames[1]);
1691 // 	assert(parser.parameterNames[0] == "Id");
1692 // 	assert(parser.parameterNames[1] == "skipName");
1693 // 	assert(parser.fromClause.length == 1);
1694 // 	assert(parser.fromClause.first.entity.name == "User");
1695 //     assert(parser.fromClause.first.entityAlias == "a");
1696 // 	assert(parser.selectClause.length == 1);
1697 // 	assert(parser.selectClause[0].prop is null);
1698 // 	assert(parser.selectClause[0].from.entity.name == "User");
1699 // 	assert(parser.orderByClause.length == 2);
1700 // 	assert(parser.orderByClause[0].prop.propertyName == "name");
1701 // 	assert(parser.orderByClause[0].from.entity.name == "User");
1702 // 	assert(parser.orderByClause[0].asc == true);
1703 // 	assert(parser.orderByClause[1].prop.propertyName == "flags");
1704 // 	assert(parser.orderByClause[1].from.entity.name == "User");
1705 // 	assert(parser.orderByClause[1].asc == false);
1706 	
1707 // 	parser = new QueryParser(schema, "SELECT a FROM User AS a WHERE ((id = :Id) OR (name LIKE 'a%' AND flags = (-5 + 7))) AND name != :skipName AND flags BETWEEN 2*2 AND 42/5 ORDER BY name, a.flags DESC");
1708 // 	assert(parser.whereClause !is null);
1709 // 	//writeln(parser.whereClause.dump(0));
1710 // 	Dialect dialect = new MySQLDialect();
1711 	
1712 // 	assert(dialect.quoteSqlString("abc") == "'abc'");
1713 // 	assert(dialect.quoteSqlString("a'b'c") == "'a\\'b\\'c'");
1714 // 	assert(dialect.quoteSqlString("a\nc") == "'a\\nc'");
1715 	
1716 // 	parser = new QueryParser(schema, "FROM User AS u WHERE id = :Id and u.name like '%test%'");
1717 // 	ParsedQuery q = parser.makeSQL(dialect);
1718 // 	//writeln(parser.whereClause.dump(0));
1719 // 	//writeln(q.hql ~ "\n=>\n" ~ q.sql);
1720 
1721 // 	//writeln(q.hql);
1722 // 	//writeln(q.sql);
1723 //     parser = new QueryParser(schema, "SELECT a FROM Person AS a LEFT JOIN a.moreInfo as b LEFT JOIN b.evenMore c WHERE a.id = :Id AND b.flags > 0 AND c.flags > 0");
1724 //     assert(parser.fromClause.hasAlias("a"));
1725 //     assert(parser.fromClause.hasAlias("b"));
1726 //     assert(parser.fromClause.findByAlias("a").entityName == "Person");
1727 //     assert(parser.fromClause.findByAlias("b").entityName == "MoreInfo");
1728 //     assert(parser.fromClause.findByAlias("b").joinType == JoinType.LeftJoin);
1729 //     assert(parser.fromClause.findByAlias("c").entityName == "EvenMoreInfo");
1730 //     // indirect JOIN
1731 //     parser = new QueryParser(schema, "SELECT a FROM Person a WHERE a.id = :Id AND a.moreInfo.evenMore.flags > 0");
1732 //     assert(parser.fromClause.hasAlias("a"));
1733 //     assert(parser.fromClause.length == 3);
1734 //     assert(parser.fromClause[0].entity.tableName == "person");
1735 //     assert(parser.fromClause[1].entity.tableName == "person_info");
1736 //     assert(parser.fromClause[1].joinType == JoinType.InnerJoin);
1737 //     assert(parser.fromClause[1].pathString == "a.moreInfo");
1738 //     assert(parser.fromClause[2].entity.tableName == "person_info2");
1739 //     assert(parser.fromClause[2].joinType == JoinType.LeftJoin);
1740 //     assert(parser.fromClause[2].pathString == "a.moreInfo.evenMore");
1741 //     // indirect JOIN, no alias
1742 //     parser = new QueryParser(schema, "FROM Person WHERE id = :Id AND moreInfo.evenMore.flags > 0");
1743 //     assert(parser.fromClause.length == 3);
1744 //     assert(parser.fromClause[0].entity.tableName == "person");
1745 //     assert(parser.fromClause[0].fetch == true);
1746 //     //writeln("select fields [" ~ to!string(parser.fromClause[0].startColumn) ~ ", " ~ to!string(parser.fromClause[0].selectedColumns) ~ "]");
1747 //     //writeln("select fields [" ~ to!string(parser.fromClause[1].startColumn) ~ ", " ~ to!string(parser.fromClause[1].selectedColumns) ~ "]");
1748 //     //writeln("select fields [" ~ to!string(parser.fromClause[2].startColumn) ~ ", " ~ to!string(parser.fromClause[2].selectedColumns) ~ "]");
1749 //     assert(parser.fromClause[0].selectedColumns == 4);
1750 //     assert(parser.fromClause[1].entity.tableName == "person_info");
1751 //     assert(parser.fromClause[1].joinType == JoinType.InnerJoin);
1752 //     assert(parser.fromClause[1].pathString == "_a1.moreInfo");
1753 //     assert(parser.fromClause[1].fetch == true);
1754 //     assert(parser.fromClause[1].selectedColumns == 2);
1755 //     assert(parser.fromClause[2].entity.tableName == "person_info2");
1756 //     assert(parser.fromClause[2].joinType == JoinType.LeftJoin);
1757 //     assert(parser.fromClause[2].pathString == "_a1.moreInfo.evenMore");
1758 //     assert(parser.fromClause[2].fetch == true);
1759 //     assert(parser.fromClause[2].selectedColumns == 3);
1760 
1761 //     q = parser.makeSQL(dialect);
1762 //     //writeln(q.hql);
1763 //     //writeln(q.sql);
1764 
1765 //     parser = new QueryParser(schema, "FROM User WHERE id in (1, 2, (3 - 1 * 25) / 2, 4 + :Id, 5)");
1766 //     //writeln(parser.whereClause.dump(0));
1767 //     q = parser.makeSQL(dialect);
1768 //     //writeln(q.hql);
1769 //     //writeln(q.sql);
1770 
1771 //     parser = new QueryParser(schema, "FROM Customer WHERE users.id = 1");
1772 //     q = parser.makeSQL(dialect);
1773 // //    writeln(q.hql);
1774 // //    writeln(q.sql);
1775 //     assert(q.sql == "SELECT _t1.id, _t1.name, _t1.zip, _t1.city, _t1.street_address, _t1.account_type_fk FROM customers AS _t1 LEFT JOIN users AS _t2 ON _t1.id=_t2.customer_fk WHERE _t2.id = 1");
1776 
1777 //     parser = new QueryParser(schema, "FROM Customer WHERE id = 1");
1778 //     q = parser.makeSQL(dialect);
1779 // //    writeln(q.hql);
1780 // //    writeln(q.sql);
1781 //     assert(q.sql == "SELECT _t1.id, _t1.name, _t1.zip, _t1.city, _t1.street_address, _t1.account_type_fk FROM customers AS _t1 WHERE _t1.id = 1");
1782 
1783 //     parser = new QueryParser(schema, "FROM User WHERE roles.id = 1");
1784 //     q = parser.makeSQL(dialect);
1785 //     //writeln(q.hql);
1786 //     //writeln(q.sql);
1787 //     assert(q.sql == "SELECT _t1.id, _t1.name, _t1.flags, _t1.comment, _t1.customer_fk FROM users AS _t1 LEFT JOIN role_users AS _t1_t2 ON _t1.id=_t1_t2.user_fk LEFT JOIN role AS _t2 ON _t1_t2.role_fk=_t2.id WHERE _t2.id = 1");
1788 
1789 //     parser = new QueryParser(schema, "FROM Role WHERE users.id = 1");
1790 //     q = parser.makeSQL(dialect);
1791 // //    writeln(q.hql);
1792 // //    writeln(q.sql);
1793 //     assert(q.sql == "SELECT _t1.id, _t1.name FROM role AS _t1 LEFT JOIN role_users AS _t1_t2 ON _t1.id=_t1_t2.role_fk LEFT JOIN users AS _t2 ON _t1_t2.user_fk=_t2.id WHERE _t2.id = 1");
1794     
1795 //     parser = new QueryParser(schema, "FROM User WHERE customer.id = 1");
1796 //     q = parser.makeSQL(dialect);
1797 // //    writeln(q.hql);
1798 // //    writeln(q.sql);
1799 //     assert(q.sql == "SELECT _t1.id, _t1.name, _t1.flags, _t1.comment, _t1.customer_fk FROM users AS _t1 LEFT JOIN customers AS _t2 ON _t1.customer_fk=_t2.id WHERE _t2.id = 1");
1800 
1801 //     parser = new QueryParser(schema, "SELECT a2 FROM User AS a1 JOIN a1.roles AS a2 WHERE a1.id = 1");
1802 //     q = parser.makeSQL(dialect);
1803 //     //writeln(q.hql);
1804 //     //writeln(q.sql);
1805 //     assert(q.sql == "SELECT _t2.id, _t2.name FROM users AS _t1 INNER JOIN role_users AS _t1_t2 ON _t1.id=_t1_t2.user_fk INNER JOIN role AS _t2 ON _t1_t2.role_fk=_t2.id WHERE _t1.id = 1");
1806 
1807 //     parser = new QueryParser(schema, "SELECT a2 FROM Customer AS a1 JOIN a1.users AS a2 WHERE a1.id = 1");
1808 //     q = parser.makeSQL(dialect);
1809 //     //writeln(q.hql);
1810 //     //writeln(q.sql);
1811 //     assert(q.sql == "SELECT _t2.id, _t2.name, _t2.flags, _t2.comment, _t2.customer_fk FROM customers AS _t1 INNER JOIN users AS _t2 ON _t1.id=_t2.customer_fk WHERE _t1.id = 1");
1812     
1813 // }