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 // For backwards compatibily
36 // 'enforceEx' will be removed with 2.089
37 static if (__VERSION__ < 2080)
38 {
39 	alias enforceHelper = enforceEx;
40 }
41 else
42 {
43 	alias enforceHelper = enforce;
44 }
45 
46 enum JoinType
47 {
48 	InnerJoin,
49 	LeftJoin,
50 }
51 
52 class FromClauseItem
53 {
54 	string entityName;
55 	const EntityInfo entity;
56 	string entityAlias;
57 	string sqlAlias;
58 	int startColumn;
59 	int selectedColumns;
60 	// for JOINs
61 	JoinType joinType = JoinType.InnerJoin;
62 	bool fetch;
63 	FromClauseItem base;
64 	const PropertyInfo baseProperty;
65 	string pathString;
66 	int index;
67 	int selectIndex;
68 
69 	string getFullPath()
70 	{
71 		if (base is null)
72 			return entityAlias;
73 		return base.getFullPath() ~ "." ~ baseProperty.propertyName;
74 	}
75 
76 	this(const EntityInfo entity, string entityAlias, JoinType joinType, bool fetch,
77 			FromClauseItem base = null, const PropertyInfo baseProperty = null)
78 	{
79 		this.entityName = entity.name;
80 		this.entity = entity;
81 		this.joinType = joinType;
82 		this.fetch = fetch;
83 		this.base = base;
84 		this.baseProperty = baseProperty;
85 		this.selectIndex = -1;
86 	}
87 
88 }
89 
90 class FromClause
91 {
92 	FromClauseItem[] items;
93 	FromClauseItem add(const EntityInfo entity, string entityAlias, JoinType joinType,
94 			bool fetch, FromClauseItem base = null, const PropertyInfo baseProperty = null)
95 	{
96 		FromClauseItem item = new FromClauseItem(entity, entityAlias, joinType,
97 				fetch, base, baseProperty);
98 		item.entityAlias = entityAlias is null ? "_a" ~ to!string(items.length + 1) : entityAlias;
99 		item.sqlAlias = "_t" ~ to!string(items.length + 1);
100 		item.index = cast(int) items.length;
101 		item.pathString = item.getFullPath();
102 		items ~= item;
103 		return item;
104 	}
105 
106 	@property size_t length()
107 	{
108 		return items.length;
109 	}
110 
111 	string getSQL()
112 	{
113 		return "";
114 	}
115 
116 	@property FromClauseItem first()
117 	{
118 		return items[0];
119 	}
120 
121 	FromClauseItem opIndex(int index)
122 	{
123 		enforceHelper!DStructException(index >= 0 && index < items.length,
124 				"FromClause index out of range: " ~ to!string(index));
125 		return items[index];
126 	}
127 
128 	FromClauseItem opIndex(string aliasName)
129 	{
130 		return findByAlias(aliasName);
131 	}
132 
133 	bool hasAlias(string aliasName)
134 	{
135 		foreach (ref m; items)
136 		{
137 			if (m.entityAlias == aliasName)
138 				return true;
139 		}
140 		return false;
141 	}
142 
143 	FromClauseItem findByAlias(string aliasName)
144 	{
145 		foreach (ref m; items)
146 		{
147 			if (m.entityAlias == aliasName)
148 				return m;
149 		}
150 		throw new QuerySyntaxException("Cannot find FROM alias by name " ~ aliasName);
151 	}
152 
153 	FromClauseItem findByPath(string path)
154 	{
155 		foreach (ref m; items)
156 		{
157 			if (m.pathString == path)
158 				return m;
159 		}
160 		return null;
161 	}
162 }
163 
164 struct OrderByClauseItem
165 {
166 	FromClauseItem from;
167 	PropertyInfo prop;
168 	bool asc;
169 }
170 
171 struct SelectClauseItem
172 {
173 	FromClauseItem from;
174 	PropertyInfo prop;
175 }
176 
177 class QueryParser
178 {
179 	string query;
180 	EntityMetaData metadata;
181 	Token[] tokens;
182 	FromClause fromClause;
183 	//FromClauseItem[] fromClause;
184 	string[] parameterNames;
185 	OrderByClauseItem[] orderByClause;
186 	SelectClauseItem[] selectClause;
187 	Token whereClause; // AST for WHERE expression
188 
189 	this(EntityMetaData metadata, string query)
190 	{
191 		this.metadata = metadata;
192 		this.query = query;
193 		fromClause = new FromClause();
194 		//writeln("tokenizing query: " ~ query);
195 		tokens = tokenize(query);
196 		//writeln("parsing query: " ~ query);
197 		parse();
198 		//writeln("parsing done");
199 	}
200 
201 	void parse()
202 	{
203 		processParameterNames(0, cast(int) tokens.length); // replace pairs {: Ident} with single Parameter token
204 		int len = cast(int) tokens.length;
205 		//writeln("Query tokens: " ~ to!string(len));
206 		int fromPos = findKeyword(KeywordType.FROM);
207 		int selectPos = findKeyword(KeywordType.SELECT);
208 		int wherePos = findKeyword(KeywordType.WHERE);
209 		int orderPos = findKeyword(KeywordType.ORDER);
210 		enforceHelper!QuerySyntaxException(fromPos >= 0, "No FROM clause in query " ~ query);
211 		enforceHelper!QuerySyntaxException(selectPos <= 0,
212 				"SELECT clause should be first - invalid query " ~ query);
213 		enforceHelper!QuerySyntaxException(wherePos == -1 || wherePos > fromPos,
214 				"Invalid WHERE position in query " ~ query);
215 		enforceHelper!QuerySyntaxException(orderPos == -1 || (orderPos < tokens.length - 2
216 				&& tokens[orderPos + 1].keyword == KeywordType.BY),
217 				"Invalid ORDER BY in query " ~ query);
218 		enforceHelper!QuerySyntaxException(orderPos == -1 || orderPos > fromPos,
219 				"Invalid ORDER BY position in query " ~ query);
220 		int fromEnd = len;
221 		if (orderPos >= 0)
222 			fromEnd = orderPos;
223 		if (wherePos >= 0)
224 			fromEnd = wherePos;
225 		int whereEnd = wherePos < 0 ? -1 : (orderPos >= 0 ? orderPos : len);
226 		int orderEnd = orderPos < 0 ? -1 : len;
227 		parseFromClause(fromPos + 1, fromEnd);
228 		if (selectPos == 0 && selectPos < fromPos - 1)
229 			parseSelectClause(selectPos + 1, fromPos);
230 		else
231 			defaultSelectClause();
232 		bool selectedEntities = validateSelectClause();
233 		if (wherePos >= 0 && whereEnd > wherePos)
234 			parseWhereClause(wherePos + 1, whereEnd);
235 		if (orderPos >= 0 && orderEnd > orderPos)
236 			parseOrderClause(orderPos + 2, orderEnd);
237 		if (selectedEntities)
238 		{
239 			processAutoFetchReferences();
240 			prepareSelectFields();
241 		}
242 	}
243 
244 	private void prepareSelectFields()
245 	{
246 		int startColumn = 1;
247 		for (int i = 0; i < fromClause.length; i++)
248 		{
249 			FromClauseItem item = fromClause[i];
250 			if (!item.fetch)
251 				continue;
252 			int count = item.entity.metadata.getFieldCount(item.entity, false);
253 			if (count > 0)
254 			{
255 				item.startColumn = startColumn;
256 				item.selectedColumns = count;
257 				startColumn += count;
258 			}
259 		}
260 	}
261 
262 	private void processAutoFetchReferences()
263 	{
264 		FromClauseItem a = selectClause[0].from;
265 		a.fetch = true;
266 		processAutoFetchReferences(a);
267 	}
268 
269 	private FromClauseItem ensureItemFetched(FromClauseItem a, const PropertyInfo p)
270 	{
271 		FromClauseItem res;
272 		string path = a.pathString ~ "." ~ p.propertyName;
273 		//writeln("ensureItemFetched " ~ path);
274 		res = fromClause.findByPath(path);
275 		if (res is null)
276 		{
277 			// autoadd join
278 			assert(p.referencedEntity !is null);
279 			res = fromClause.add(p.referencedEntity, null, p.nullable
280 					? JoinType.LeftJoin : JoinType.InnerJoin, true, a, p);
281 		}
282 		else
283 		{
284 			// force fetch
285 			res.fetch = true;
286 		}
287 		bool selectFound = false;
288 		foreach (s; selectClause)
289 		{
290 			if (s.from == res)
291 			{
292 				selectFound = true;
293 				break;
294 			}
295 		}
296 		if (!selectFound)
297 		{
298 			SelectClauseItem item;
299 			item.from = res;
300 			item.prop = null;
301 			selectClause ~= item;
302 		}
303 		return res;
304 	}
305 
306 	private bool isBackReferenceProperty(FromClauseItem a, const PropertyInfo p)
307 	{
308 		if (a.base is null)
309 			return false;
310 		auto baseEntity = a.base.entity;
311 		assert(baseEntity !is null);
312 		if (p.referencedEntity != baseEntity)
313 			return false;
314 
315 		if (p.referencedProperty !is null && p.referencedProperty == a.baseProperty)
316 			return true;
317 		if (a.baseProperty.referencedProperty !is null && p == a.baseProperty.referencedProperty)
318 			return true;
319 		return false;
320 	}
321 
322 	private void processAutoFetchReferences(FromClauseItem a)
323 	{
324 		foreach (p; a.entity.properties)
325 		{
326 			if (p.lazyLoad)
327 				continue;
328 			if (p.oneToOne && !isBackReferenceProperty(a, p))
329 			{
330 				FromClauseItem res = ensureItemFetched(a, p);
331 				processAutoFetchReferences(res);
332 			}
333 		}
334 	}
335 
336 	private void updateEntity(const EntityInfo entity, string name)
337 	{
338 		foreach (t; tokens)
339 		{
340 			if (t.type == TokenType.Ident && t.text == name)
341 			{
342 				t.entity = cast(EntityInfo) entity;
343 				t.type = TokenType.Entity;
344 			}
345 		}
346 	}
347 
348 	private void updateAlias(const EntityInfo entity, string name)
349 	{
350 		foreach (t; tokens)
351 		{
352 			if (t.type == TokenType.Ident && t.text == name)
353 			{
354 				t.entity = cast(EntityInfo) entity;
355 				t.type = TokenType.Alias;
356 			}
357 		}
358 	}
359 
360 	private void splitCommaDelimitedList(int start, int end, void delegate(int, int) callback)
361 	{
362 		//writeln("SPLIT " ~ to!string(start) ~ " .. " ~ to!string(end));
363 		int len = cast(int) tokens.length;
364 		int p = start;
365 		for (int i = start; i < end; i++)
366 		{
367 			if (tokens[i].type == TokenType.Comma || i == end - 1)
368 			{
369 				enforceHelper!QuerySyntaxException(tokens[i].type != TokenType.Comma
370 						|| i != end - 1,
371 						"Invalid comma at end of list" ~ errorContext(tokens[start]));
372 				int endp = i < end - 1 ? i : end;
373 				enforceHelper!QuerySyntaxException(endp > p,
374 						"Invalid comma delimited list" ~ errorContext(tokens[start]));
375 				callback(p, endp);
376 				p = i + 1;
377 			}
378 		}
379 	}
380 
381 	private int parseFieldRef(int start, int end, ref string[] path)
382 	{
383 		int pos = start;
384 		while (pos < end)
385 		{
386 			if (tokens[pos].type == TokenType.Ident || tokens[pos].type == TokenType.Alias)
387 			{
388 				enforceHelper!QuerySyntaxException(path.length == 0 || tokens[pos].type != TokenType.Alias,
389 						"Alias is allowed only as first item" ~ errorContext(tokens[pos]));
390 				path ~= tokens[pos].text;
391 				pos++;
392 				if (pos == end || tokens[pos].type != TokenType.Dot)
393 					return pos;
394 				if (pos == end - 1 || tokens[pos + 1].type != TokenType.Ident)
395 					return pos;
396 				pos++;
397 			}
398 			else
399 			{
400 				break;
401 			}
402 		}
403 		enforceHelper!QuerySyntaxException(tokens[pos].type != TokenType.Dot,
404 				"Unexpected dot at end in field list" ~ errorContext(tokens[pos]));
405 		enforceHelper!QuerySyntaxException(path.length > 0,
406 				"Empty field list" ~ errorContext(tokens[pos]));
407 		return pos;
408 	}
409 
410 	private void parseFirstFromClause(int start, int end, out int pos)
411 	{
412 		enforceHelper!QuerySyntaxException(start < end,
413 				"Invalid FROM clause " ~ errorContext(tokens[start]));
414 		// minimal support:
415 		//    Entity
416 		//    Entity alias
417 		//    Entity AS alias
418 		enforceHelper!QuerySyntaxException(tokens[start].type == TokenType.Ident,
419 				"Entity name identifier expected in FROM clause" ~ errorContext(tokens[start]));
420 		string entityName = cast(string) tokens[start].text;
421 		auto ei = metadata.findEntity(entityName);
422 		updateEntity(ei, entityName);
423 		string aliasName = null;
424 		int p = start + 1;
425 		if (p < end && tokens[p].type == TokenType.Keyword && tokens[p].keyword == KeywordType.AS)
426 			p++;
427 		if (p < end)
428 		{
429 			enforceHelper!QuerySyntaxException(tokens[p].type == TokenType.Ident,
430 					"Alias name identifier expected in FROM clause" ~ errorContext(tokens[p]));
431 			aliasName = cast(string) tokens[p].text;
432 			p++;
433 		}
434 		if (aliasName != null)
435 			updateAlias(ei, aliasName);
436 		fromClause.add(ei, aliasName, JoinType.InnerJoin, false);
437 		pos = p;
438 	}
439 
440 	void appendFromClause(Token context, string[] path, string aliasName,
441 			JoinType joinType, bool fetch)
442 	{
443 		int p = 0;
444 		enforceHelper!QuerySyntaxException(fromClause.hasAlias(path[p]),
445 				"Unknown alias " ~ path[p] ~ " in FROM clause" ~ errorContext(context));
446 		FromClauseItem baseClause = findFromClauseByAlias(path[p]);
447 		//string pathString = path[p];
448 		p++;
449 		while (true)
450 		{
451 			auto baseEntity = baseClause.entity;
452 			enforceHelper!QuerySyntaxException(p < path.length,
453 					"Property name expected in FROM clause" ~ errorContext(context));
454 			string propertyName = path[p++];
455 			auto property = baseEntity[propertyName];
456 			auto referencedEntity = property.referencedEntity;
457 			assert(referencedEntity !is null);
458 			enforceHelper!QuerySyntaxException(!property.simple,
459 					"Simple property " ~ propertyName ~ " cannot be used in JOIN" ~ errorContext(
460 						context));
461 			enforceHelper!QuerySyntaxException(!property.embedded,
462 					"Embedded property " ~ propertyName ~ " cannot be used in JOIN" ~ errorContext(
463 						context));
464 			bool last = (p == path.length);
465 			FromClauseItem item = fromClause.add(referencedEntity, last
466 					? aliasName : null, joinType, fetch, baseClause, property);
467 			if (last && aliasName !is null)
468 				updateAlias(referencedEntity, item.entityAlias);
469 			baseClause = item;
470 			if (last)
471 				break;
472 		}
473 	}
474 
475 	void parseFromClause(int start, int end)
476 	{
477 		int p = start;
478 		parseFirstFromClause(start, end, p);
479 		while (p < end)
480 		{
481 			Token context = tokens[p];
482 			JoinType joinType = JoinType.InnerJoin;
483 			if (tokens[p].keyword == KeywordType.LEFT)
484 			{
485 				joinType = JoinType.LeftJoin;
486 				p++;
487 			}
488 			else if (tokens[p].keyword == KeywordType.INNER)
489 			{
490 				p++;
491 			}
492 			enforceHelper!QuerySyntaxException(p < end && tokens[p].keyword == KeywordType.JOIN,
493 					"Invalid FROM clause" ~ errorContext(tokens[p]));
494 			p++;
495 			enforceHelper!QuerySyntaxException(p < end,
496 					"Invalid FROM clause - incomplete JOIN" ~ errorContext(tokens[p]));
497 			bool fetch = false;
498 			if (tokens[p].keyword == KeywordType.FETCH)
499 			{
500 				fetch = true;
501 				p++;
502 				enforceHelper!QuerySyntaxException(p < end,
503 						"Invalid FROM clause - incomplete JOIN" ~ errorContext(tokens[p]));
504 			}
505 			string[] path;
506 			p = parseFieldRef(p, end, path);
507 			string aliasName;
508 			bool hasAS = false;
509 			if (p < end && tokens[p].keyword == KeywordType.AS)
510 			{
511 				p++;
512 				hasAS = true;
513 			}
514 			enforceHelper!QuerySyntaxException(p < end && tokens[p].type == TokenType.Ident,
515 					"Invalid FROM clause - no alias in JOIN" ~ errorContext(tokens[p]));
516 			aliasName = tokens[p].text;
517 			p++;
518 			appendFromClause(context, path, aliasName, joinType, fetch);
519 		}
520 		enforceHelper!QuerySyntaxException(p == end, "Invalid FROM clause" ~ errorContext(tokens[p]));
521 	}
522 
523 	// in pairs {: Ident} replace type of ident with Parameter 
524 	void processParameterNames(int start, int end)
525 	{
526 		for (int i = start; i < end; i++)
527 		{
528 			if (tokens[i].type == TokenType.Parameter)
529 			{
530 				parameterNames ~= cast(string) tokens[i].text;
531 			}
532 		}
533 	}
534 
535 	FromClauseItem findFromClauseByAlias(string aliasName)
536 	{
537 		return fromClause.findByAlias(aliasName);
538 	}
539 
540 	void addSelectClauseItem(string aliasName, string[] propertyNames)
541 	{
542 		//writeln("addSelectClauseItem alias=" ~ aliasName ~ " properties=" ~ to!string(propertyNames));
543 		FromClauseItem from = aliasName == null ? fromClause.first
544 			: findFromClauseByAlias(aliasName);
545 		SelectClauseItem item;
546 		item.from = from;
547 		item.prop = null;
548 		EntityInfo ei = cast(EntityInfo) from.entity;
549 		if (propertyNames.length > 0)
550 		{
551 			item.prop = cast(PropertyInfo) ei.findProperty(propertyNames[0]);
552 			propertyNames.popFront();
553 			while (item.prop.embedded)
554 			{
555 				//writeln("Embedded property " ~ item.prop.propertyName ~ " of type " ~ item.prop.referencedEntityName);
556 				ei = cast(EntityInfo) item.prop.referencedEntity;
557 				enforceHelper!QuerySyntaxException(propertyNames.length > 0,
558 						"@Embedded field property name should be specified when selecting "
559 						~ aliasName ~ "." ~ item.prop.propertyName);
560 				item.prop = cast(PropertyInfo) ei.findProperty(propertyNames[0]);
561 				propertyNames.popFront();
562 			}
563 		}
564 		enforceHelper!QuerySyntaxException(propertyNames.length == 0,
565 				"Extra field names in SELECT clause in query " ~ query);
566 		selectClause ~= item;
567 		//insertInPlace(selectClause, 0, item);
568 	}
569 
570 	void addOrderByClauseItem(string aliasName, string propertyName, bool asc)
571 	{
572 		FromClauseItem from = aliasName == null ? fromClause.first
573 			: findFromClauseByAlias(aliasName);
574 		OrderByClauseItem item;
575 		item.from = from;
576 		item.prop = cast(PropertyInfo) from.entity.findProperty(propertyName);
577 		item.asc = asc;
578 		orderByClause ~= item;
579 		//insertInPlace(orderByClause, 0, item);
580 	}
581 
582 	void parseOrderByClauseItem(int start, int end)
583 	{
584 		// for each comma delimited item
585 		// in current version it can only be
586 		// {property}  or  {alias . property} optionally followed by ASC or DESC
587 		//writeln("ORDER BY ITEM: " ~ to!string(start) ~ " .. " ~ to!string(end));
588 		bool asc = true;
589 		if (tokens[end - 1].type == TokenType.Keyword && tokens[end - 1].keyword == KeywordType.ASC)
590 		{
591 			end--;
592 		}
593 		else if (tokens[end - 1].type == TokenType.Keyword
594 				&& tokens[end - 1].keyword == KeywordType.DESC)
595 		{
596 			asc = false;
597 			end--;
598 		}
599 		enforceHelper!QuerySyntaxException(start < end,
600 				"Empty ORDER BY clause item" ~ errorContext(tokens[start]));
601 		if (start == end - 1)
602 		{
603 			// no alias
604 			enforceHelper!QuerySyntaxException(tokens[start].type == TokenType.Ident,
605 					"Property name expected in ORDER BY clause" ~ errorContext(tokens[start]));
606 			addOrderByClauseItem(null, cast(string) tokens[start].text, asc);
607 		}
608 		else if (start == end - 3)
609 		{
610 			enforceHelper!QuerySyntaxException(tokens[start].type == TokenType.Alias,
611 					"Entity alias expected in ORDER BY clause" ~ errorContext(tokens[start]));
612 			enforceHelper!QuerySyntaxException(tokens[start + 1].type == TokenType.Dot,
613 					"Dot expected after entity alias in ORDER BY clause" ~ errorContext(
614 						tokens[start]));
615 			enforceHelper!QuerySyntaxException(tokens[start + 2].type == TokenType.Ident,
616 					"Property name expected after entity alias in ORDER BY clause" ~ errorContext(
617 						tokens[start]));
618 			addOrderByClauseItem(cast(string) tokens[start].text,
619 					cast(string) tokens[start + 2].text, asc);
620 		}
621 		else
622 		{
623 			//writeln("range: " ~ to!string(start) ~ " .. " ~ to!string(end));
624 			enforceHelper!QuerySyntaxException(false,
625 					"Invalid ORDER BY clause (expected {property [ASC | DESC]} or {alias.property [ASC | DESC]} )"
626 					~ errorContext(tokens[start]));
627 		}
628 	}
629 
630 	void parseSelectClauseItem(int start, int end)
631 	{
632 		// for each comma delimited item
633 		// in current version it can only be
634 		// {property}  or  {alias . property}
635 		//writeln("SELECT ITEM: " ~ to!string(start) ~ " .. " ~ to!string(end));
636 		enforceHelper!QuerySyntaxException(tokens[start].type == TokenType.Ident || tokens[start].type == TokenType.Alias,
637 				"Property name or alias expected in SELECT clause in query " ~ query ~ errorContext(
638 					tokens[start]));
639 		string aliasName;
640 		int p = start;
641 		if (tokens[p].type == TokenType.Alias)
642 		{
643 			//writeln("select clause alias: " ~ tokens[p].text ~ " query: " ~ query);
644 			aliasName = cast(string) tokens[p].text;
645 			p++;
646 			enforceHelper!QuerySyntaxException(p == end || tokens[p].type == TokenType.Dot,
647 					"SELECT clause item is invalid (only  [alias.]field{[.field2]}+ allowed) " ~ errorContext(
648 						tokens[start]));
649 			if (p < end - 1 && tokens[p].type == TokenType.Dot)
650 				p++;
651 		}
652 		else
653 		{
654 			//writeln("select clause non-alias: " ~ tokens[p].text ~ " query: " ~ query);
655 		}
656 		string[] fieldNames;
657 		while (p < end && tokens[p].type == TokenType.Ident)
658 		{
659 			fieldNames ~= tokens[p].text;
660 			p++;
661 			if (p > end - 1 || tokens[p].type != TokenType.Dot)
662 				break;
663 			// skipping dot
664 			p++;
665 		}
666 		//writeln("parseSelectClauseItem pos=" ~ to!string(p) ~ " end=" ~ to!string(end));
667 		enforceHelper!QuerySyntaxException(p >= end,
668 				"SELECT clause item is invalid (only  [alias.]field{[.field2]}+ allowed) " ~ errorContext(
669 					tokens[start]));
670 		addSelectClauseItem(aliasName, fieldNames);
671 	}
672 
673 	void parseSelectClause(int start, int end)
674 	{
675 		enforceHelper!QuerySyntaxException(start < end,
676 				"Invalid SELECT clause" ~ errorContext(tokens[start]));
677 		splitCommaDelimitedList(start, end, &parseSelectClauseItem);
678 	}
679 
680 	void defaultSelectClause()
681 	{
682 		addSelectClauseItem(fromClause.first.entityAlias, null);
683 	}
684 
685 	bool validateSelectClause()
686 	{
687 		enforceHelper!QuerySyntaxException(selectClause != null
688 				&& selectClause.length > 0, "Invalid SELECT clause");
689 		int aliasCount = 0;
690 		int fieldCount = 0;
691 		foreach (a; selectClause)
692 		{
693 			if (a.prop !is null)
694 				fieldCount++;
695 			else
696 				aliasCount++;
697 		}
698 		enforceHelper!QuerySyntaxException((aliasCount == 1 && fieldCount == 0) || (aliasCount == 0
699 				&& fieldCount > 0), "You should either use single entity alias or one or more properties in SELECT clause. Don't mix objects with primitive fields");
700 		return aliasCount > 0;
701 	}
702 
703 	void parseWhereClause(int start, int end)
704 	{
705 		enforceHelper!QuerySyntaxException(start < end,
706 				"Invalid WHERE clause" ~ errorContext(tokens[start]));
707 		whereClause = new Token(tokens[start].pos, TokenType.Expression, tokens, start, end);
708 		//writeln("before convert fields:\n" ~ whereClause.dump(0));
709 		convertFields(whereClause.children);
710 		//writeln("after convertFields before convertIsNullIsNotNull:\n" ~ whereClause.dump(0));
711 		convertIsNullIsNotNull(whereClause.children);
712 		//writeln("after convertIsNullIsNotNull\n" ~ whereClause.dump(0));
713 		convertUnaryPlusMinus(whereClause.children);
714 		//writeln("after convertUnaryPlusMinus\n" ~ whereClause.dump(0));
715 		foldBraces(whereClause.children);
716 		//writeln("after foldBraces\n" ~ whereClause.dump(0));
717 		foldOperators(whereClause.children);
718 		//writeln("after foldOperators\n" ~ whereClause.dump(0));
719 		dropBraces(whereClause.children);
720 		//writeln("after dropBraces\n" ~ whereClause.dump(0));
721 	}
722 
723 	void foldBraces(ref Token[] items)
724 	{
725 		while (true)
726 		{
727 			if (items.length == 0)
728 				return;
729 			int lastOpen = -1;
730 			int firstClose = -1;
731 			for (int i = 0; i < items.length; i++)
732 			{
733 				if (items[i].type == TokenType.OpenBracket)
734 				{
735 					lastOpen = i;
736 				}
737 				if (items[i].type == TokenType.CloseBracket)
738 				{
739 					firstClose = i;
740 					break;
741 				}
742 			}
743 			if (lastOpen == -1 && firstClose == -1)
744 				return;
745 			//writeln("folding braces " ~ to!string(lastOpen) ~ " .. " ~ to!string(firstClose));
746 			enforceHelper!QuerySyntaxException(lastOpen >= 0 && lastOpen < firstClose,
747 					"Unpaired braces in WHERE clause" ~ errorContext(tokens[lastOpen]));
748 			Token folded = new Token(items[lastOpen].pos, TokenType.Braces,
749 					items, lastOpen + 1, firstClose);
750 			//			size_t oldlen = items.length;
751 			//			int removed = firstClose - lastOpen;
752 			replaceInPlace(items, lastOpen, firstClose + 1, [folded]);
753 			//			assert(items.length == oldlen - removed);
754 			foldBraces(folded.children);
755 		}
756 	}
757 
758 	static void dropBraces(ref Token[] items)
759 	{
760 		foreach (t; items)
761 		{
762 			if (t.children.length > 0)
763 				dropBraces(t.children);
764 		}
765 		for (int i = 0; i < items.length; i++)
766 		{
767 			if (items[i].type != TokenType.Braces)
768 				continue;
769 			if (items[i].children.length == 1)
770 			{
771 				Token t = items[i].children[0];
772 				replaceInPlace(items, i, i + 1, [t]);
773 			}
774 		}
775 	}
776 
777 	void convertIsNullIsNotNull(ref Token[] items)
778 	{
779 		for (int i = cast(int) items.length - 2; i >= 0; i--)
780 		{
781 			if (items[i].type != TokenType.Operator || items[i + 1].type != TokenType.Keyword)
782 				continue;
783 			if (items[i].operator == OperatorType.IS && items[i + 1].keyword == KeywordType.NULL)
784 			{
785 				Token folded = new Token(items[i].pos, OperatorType.IS_NULL, "IS NULL");
786 				replaceInPlace(items, i, i + 2, [folded]);
787 				i -= 2;
788 			}
789 		}
790 		for (int i = cast(int) items.length - 3; i >= 0; i--)
791 		{
792 			if (items[i].type != TokenType.Operator
793 					|| items[i + 1].type != TokenType.Operator
794 					|| items[i + 2].type != TokenType.Keyword)
795 				continue;
796 			if (items[i].operator == OperatorType.IS
797 					&& items[i + 1].operator == OperatorType.NOT
798 					&& items[i + 2].keyword == KeywordType.NULL)
799 			{
800 				Token folded = new Token(items[i].pos, OperatorType.IS_NOT_NULL, "IS NOT NULL");
801 				replaceInPlace(items, i, i + 3, [folded]);
802 				i -= 3;
803 			}
804 		}
805 	}
806 
807 	void convertFields(ref Token[] items)
808 	{
809 		while (true)
810 		{
811 			int p = -1;
812 			for (int i = 0; i < items.length; i++)
813 			{
814 				if (items[i].type != TokenType.Ident && items[i].type != TokenType.Alias)
815 					continue;
816 				p = i;
817 				break;
818 			}
819 			if (p == -1)
820 				return;
821 			// found identifier at position p
822 			string[] idents;
823 			int lastp = p;
824 			idents ~= items[p].text;
825 			for (int i = p + 1; i < items.length - 1; i += 2)
826 			{
827 				if (items[i].type != TokenType.Dot)
828 					break;
829 				enforceHelper!QuerySyntaxException(i < items.length - 1 && items[i + 1].type == TokenType.Ident,
830 						"Syntax error in WHERE condition - no property name after . " ~ errorContext(
831 							items[p]));
832 				lastp = i + 1;
833 				idents ~= items[i + 1].text;
834 			}
835 			string fullName;
836 			FromClauseItem a;
837 			if (items[p].type == TokenType.Alias)
838 			{
839 				a = findFromClauseByAlias(idents[0]);
840 				idents.popFront();
841 			}
842 			else
843 			{
844 				// use first FROM clause if alias is not specified
845 				a = fromClause.first;
846 			}
847 			string aliasName = a.entityAlias;
848 			EntityInfo ei = cast(EntityInfo) a.entity;
849 			enforceHelper!QuerySyntaxException(idents.length > 0,
850 					"Syntax error in WHERE condition - alias w/o property name: " ~ aliasName ~ errorContext(
851 						items[p]));
852 			PropertyInfo pi;
853 			fullName = aliasName;
854 			while (true)
855 			{
856 				string propertyName = idents[0];
857 				idents.popFront();
858 				fullName ~= "." ~ propertyName;
859 				pi = cast(PropertyInfo) ei.findProperty(propertyName);
860 				while (pi.embedded)
861 				{ // loop to allow nested @Embedded
862 					enforceHelper!QuerySyntaxException(idents.length > 0, "Syntax error in WHERE condition - @Embedded property reference should include reference to @Embeddable property " ~ aliasName ~ errorContext(
863 							items[p]));
864 					propertyName = idents[0];
865 					idents.popFront();
866 					pi = cast(PropertyInfo) pi.referencedEntity.findProperty(propertyName);
867 					fullName = fullName ~ "." ~ propertyName;
868 				}
869 				if (idents.length == 0)
870 					break;
871 				if (idents.length > 0)
872 				{
873 					// more field names
874 					string pname = idents[0];
875 					enforceHelper!QuerySyntaxException(pi.referencedEntity !is null,
876 							"Unexpected extra field name " ~ pname ~ " - property " ~ propertyName
877 							~ " doesn't content subproperties " ~ errorContext(items[p]));
878 					ei = cast(EntityInfo) pi.referencedEntity;
879 					FromClauseItem newClause = fromClause.findByPath(fullName);
880 					if (newClause is null)
881 					{
882 						// autogenerate FROM clause
883 						newClause = fromClause.add(ei, null, pi.nullable
884 								? JoinType.LeftJoin : JoinType.InnerJoin, false, a, pi);
885 					}
886 					a = newClause;
887 				}
888 			}
889 			enforceHelper!QuerySyntaxException(idents.length == 0,
890 					"Unexpected extra field name " ~ idents[0] ~ errorContext(items[p]));
891 			//writeln("full name = " ~ fullName);
892 			Token t = new Token(items[p].pos, TokenType.Field, fullName);
893 			t.entity = cast(EntityInfo) ei;
894 			t.field = cast(PropertyInfo) pi;
895 			t.from = a;
896 			replaceInPlace(items, p, lastp + 1, [t]);
897 		}
898 	}
899 
900 	static void convertUnaryPlusMinus(ref Token[] items)
901 	{
902 		foreach (t; items)
903 		{
904 			if (t.children.length > 0)
905 				convertUnaryPlusMinus(t.children);
906 		}
907 		for (int i = 0; i < items.length; i++)
908 		{
909 			if (items[i].type != TokenType.Operator)
910 				continue;
911 			OperatorType op = items[i].operator;
912 			if (op == OperatorType.ADD || op == OperatorType.SUB)
913 			{
914 				// convert + and - to unary form if necessary
915 				if (i == 0 || !items[i - 1].isExpression())
916 				{
917 					items[i].operator = (op == OperatorType.ADD)
918 						? OperatorType.UNARY_PLUS : OperatorType.UNARY_MINUS;
919 				}
920 			}
921 		}
922 	}
923 
924 	string errorContext(Token token)
925 	{
926 		return " near `" ~ query[token.pos .. $] ~ "` in query `" ~ query ~ "`";
927 	}
928 
929 	void foldCommaSeparatedList(Token braces)
930 	{
931 		// fold inside braces
932 		Token[] items = braces.children;
933 		int start = 0;
934 		Token[] list;
935 		for (int i = 0; i <= items.length; i++)
936 		{
937 			if (i == items.length || items[i].type == TokenType.Comma)
938 			{
939 				enforceHelper!QuerySyntaxException(i > start,
940 						"Empty item in comma separated list" ~ errorContext(items[i]));
941 				enforceHelper!QuerySyntaxException(i != items.length - 1,
942 						"Empty item in comma separated list" ~ errorContext(items[i]));
943 				Token item = new Token(items[start].pos, TokenType.Expression,
944 						braces.children, start, i);
945 				foldOperators(item.children);
946 				enforceHelper!QuerySyntaxException(item.children.length == 1,
947 						"Invalid expression in list item" ~ errorContext(items[i]));
948 				list ~= item.children[0];
949 				start = i + 1;
950 			}
951 		}
952 		enforceHelper!QuerySyntaxException(list.length > 0, "Empty list" ~ errorContext(items[0]));
953 		braces.type = TokenType.CommaDelimitedList;
954 		braces.children = list;
955 	}
956 
957 	void foldOperators(ref Token[] items)
958 	{
959 		foreach (t; items)
960 		{
961 			if (t.children.length > 0)
962 				foldOperators(t.children);
963 		}
964 		while (true)
965 		{
966 			//
967 			int bestOpPosition = -1;
968 			int bestOpPrecedency = -1;
969 			OperatorType t = OperatorType.NONE;
970 			for (int i = 0; i < items.length; i++)
971 			{
972 				if (items[i].type != TokenType.Operator)
973 					continue;
974 				int p = operatorPrecedency(items[i].operator);
975 				if (p > bestOpPrecedency)
976 				{
977 					bestOpPrecedency = p;
978 					bestOpPosition = i;
979 					t = items[i].operator;
980 				}
981 			}
982 			if (bestOpPrecedency == -1)
983 				return;
984 			//writeln("Found op " ~ items[bestOpPosition].toString() ~ " at position " ~ to!string(bestOpPosition) ~ " with priority " ~ to!string(bestOpPrecedency));
985 			if (t == OperatorType.NOT || t == OperatorType.UNARY_PLUS
986 					|| t == OperatorType.UNARY_MINUS)
987 			{
988 				// fold unary
989 				enforceHelper!QuerySyntaxException(bestOpPosition < items.length
990 						&& items[bestOpPosition + 1].isExpression(),
991 						"Syntax error in WHERE condition " ~ errorContext(items[bestOpPosition]));
992 				Token folded = new Token(items[bestOpPosition].pos, t,
993 						items[bestOpPosition].text, items[bestOpPosition + 1]);
994 				replaceInPlace(items, bestOpPosition, bestOpPosition + 2, [
995 						folded
996 						]);
997 			}
998 			else if (t == OperatorType.IS_NULL || t == OperatorType.IS_NOT_NULL)
999 			{
1000 				// fold unary
1001 				enforceHelper!QuerySyntaxException(bestOpPosition > 0 && items[bestOpPosition - 1].isExpression(),
1002 						"Syntax error in WHERE condition " ~ errorContext(items[bestOpPosition]));
1003 				Token folded = new Token(items[bestOpPosition - 1].pos, t,
1004 						items[bestOpPosition].text, items[bestOpPosition - 1]);
1005 				replaceInPlace(items, bestOpPosition - 1, bestOpPosition + 1, [
1006 						folded
1007 						]);
1008 			}
1009 			else if (t == OperatorType.BETWEEN)
1010 			{
1011 				// fold  X BETWEEN A AND B
1012 				enforceHelper!QuerySyntaxException(bestOpPosition > 0,
1013 						"Syntax error in WHERE condition - no left arg for BETWEEN operator");
1014 				enforceHelper!QuerySyntaxException(bestOpPosition < items.length - 1,
1015 						"Syntax error in WHERE condition - no min bound for BETWEEN operator " ~ errorContext(
1016 							items[bestOpPosition]));
1017 				enforceHelper!QuerySyntaxException(bestOpPosition < items.length - 3,
1018 						"Syntax error in WHERE condition - no max bound for BETWEEN operator " ~ errorContext(
1019 							items[bestOpPosition]));
1020 				enforceHelper!QuerySyntaxException(items[bestOpPosition + 2].operator == OperatorType.AND,
1021 						"Syntax error in WHERE condition - no max bound for BETWEEN operator" ~ errorContext(
1022 							items[bestOpPosition]));
1023 				Token folded = new Token(items[bestOpPosition - 1].pos, t,
1024 						items[bestOpPosition].text, items[bestOpPosition - 1]);
1025 				folded.children ~= items[bestOpPosition + 1];
1026 				folded.children ~= items[bestOpPosition + 3];
1027 				replaceInPlace(items, bestOpPosition - 1, bestOpPosition + 4, [
1028 						folded
1029 						]);
1030 			}
1031 			else if (t == OperatorType.IN)
1032 			{
1033 				// fold  X IN (A, B, ...)
1034 				enforceHelper!QuerySyntaxException(bestOpPosition > 0,
1035 						"Syntax error in WHERE condition - no left arg for IN operator");
1036 				enforceHelper!QuerySyntaxException(bestOpPosition < items.length - 1,
1037 						"Syntax error in WHERE condition - no value list for IN operator " ~ errorContext(
1038 							items[bestOpPosition]));
1039 				enforceHelper!QuerySyntaxException(items[bestOpPosition + 1].type == TokenType.Braces,
1040 						"Syntax error in WHERE condition - no value list in braces for IN operator" ~ errorContext(
1041 							items[bestOpPosition]));
1042 				Token folded = new Token(items[bestOpPosition - 1].pos, t,
1043 						items[bestOpPosition].text, items[bestOpPosition - 1]);
1044 				folded.children ~= items[bestOpPosition + 1];
1045 				foldCommaSeparatedList(items[bestOpPosition + 1]);
1046 				replaceInPlace(items, bestOpPosition - 1, bestOpPosition + 2, [
1047 						folded
1048 						]);
1049 				// fold value list
1050 				//writeln("IN operator found: " ~ folded.dump(3));
1051 			}
1052 			else
1053 			{
1054 				// fold binary
1055 				enforceHelper!QuerySyntaxException(bestOpPosition > 0,
1056 						"Syntax error in WHERE condition - no left arg for binary operator " ~ errorContext(
1057 							items[bestOpPosition]));
1058 				enforceHelper!QuerySyntaxException(bestOpPosition < items.length - 1,
1059 						"Syntax error in WHERE condition - no right arg for binary operator " ~ errorContext(
1060 							items[bestOpPosition]));
1061 				//writeln("binary op " ~ items[bestOpPosition - 1].toString() ~ " " ~ items[bestOpPosition].toString() ~ " " ~ items[bestOpPosition + 1].toString());
1062 				enforceHelper!QuerySyntaxException(items[bestOpPosition - 1].isExpression(),
1063 						"Syntax error in WHERE condition - wrong type of left arg for binary operator " ~ errorContext(
1064 							items[bestOpPosition]));
1065 				enforceHelper!QuerySyntaxException(items[bestOpPosition + 1].isExpression(),
1066 						"Syntax error in WHERE condition - wrong type of right arg for binary operator " ~ errorContext(
1067 							items[bestOpPosition]));
1068 				Token folded = new Token(items[bestOpPosition - 1].pos, t,
1069 						items[bestOpPosition].text, items[bestOpPosition - 1],
1070 						items[bestOpPosition + 1]);
1071 				auto oldlen = items.length;
1072 				replaceInPlace(items, bestOpPosition - 1, bestOpPosition + 2, [
1073 						folded
1074 						]);
1075 				assert(items.length == oldlen - 2);
1076 			}
1077 		}
1078 	}
1079 
1080 	void parseOrderClause(int start, int end)
1081 	{
1082 		enforceHelper!QuerySyntaxException(start < end,
1083 				"Invalid ORDER BY clause" ~ errorContext(tokens[start]));
1084 		splitCommaDelimitedList(start, end, &parseOrderByClauseItem);
1085 	}
1086 
1087 	/// returns position of keyword in tokens array, -1 if not found
1088 	int findKeyword(KeywordType k, int startFrom = 0)
1089 	{
1090 		for (int i = startFrom; i < tokens.length; i++)
1091 		{
1092 			if (tokens[i].type == TokenType.Keyword && tokens[i].keyword == k)
1093 				return i;
1094 		}
1095 		return -1;
1096 	}
1097 
1098 	int addSelectSQL(Dialect dialect, ParsedQuery res, string tableName,
1099 			bool first, const EntityInfo ei)
1100 	{
1101 		int colCount = 0;
1102 		for (int j = 0; j < ei.getPropertyCount(); j++)
1103 		{
1104 			PropertyInfo f = cast(PropertyInfo) ei.getProperty(j);
1105 			string fieldName = f.columnName;
1106 			if (f.embedded)
1107 			{
1108 				// put embedded cols here
1109 				colCount += addSelectSQL(dialect, res, tableName, first
1110 						&& colCount == 0, f.referencedEntity);
1111 				continue;
1112 			}
1113 			else if (f.oneToOne)
1114 			{
1115 			}
1116 			else
1117 			{
1118 			}
1119 			if (fieldName is null)
1120 				continue;
1121 			if (!first || colCount > 0)
1122 			{
1123 				res.appendSQL(", ");
1124 			}
1125 			else
1126 				first = false;
1127 
1128 			res.appendSQL(tableName ~ "." ~ dialect.quoteIfNeeded(fieldName));
1129 			colCount++;
1130 		}
1131 		return colCount;
1132 	}
1133 
1134 	void addSelectSQL(Dialect dialect, ParsedQuery res)
1135 	{
1136 		res.appendSQL("SELECT ");
1137 		bool first = true;
1138 		assert(selectClause.length > 0);
1139 		int colCount = 0;
1140 		foreach (i, s; selectClause)
1141 		{
1142 			s.from.selectIndex = cast(int) i;
1143 		}
1144 		if (selectClause[0].prop is null)
1145 		{
1146 			// object alias is specified: add all properties of object
1147 			//writeln("selected entity count: " ~ to!string(selectClause.length));
1148 			res.setEntity(selectClause[0].from.entity);
1149 			for (int i = 0; i < fromClause.length; i++)
1150 			{
1151 				FromClauseItem from = fromClause[i];
1152 				if (!from.fetch)
1153 					continue;
1154 				string tableName = from.sqlAlias;
1155 				assert(from !is null);
1156 				assert(from.entity !is null);
1157 				colCount += addSelectSQL(dialect, res, tableName, colCount == 0, from.entity);
1158 			}
1159 		}
1160 		else
1161 		{
1162 			// individual fields specified
1163 			res.setEntity(null);
1164 			foreach (a; selectClause)
1165 			{
1166 				string fieldName = a.prop.columnName;
1167 				string tableName = a.from.sqlAlias;
1168 				if (!first)
1169 				{
1170 					res.appendSQL(", ");
1171 				}
1172 				else
1173 					first = false;
1174 				res.appendSQL(tableName ~ "." ~ dialect.quoteIfNeeded(fieldName));
1175 				colCount++;
1176 			}
1177 		}
1178 		res.setColCount(colCount);
1179 		res.setSelect(selectClause);
1180 	}
1181 
1182 	void addFromSQL(Dialect dialect, ParsedQuery res)
1183 	{
1184 		res.setFromClause(fromClause);
1185 		res.appendSpace();
1186 		res.appendSQL("FROM ");
1187 		res.appendSQL(dialect.quoteIfNeeded(
1188 				fromClause.first.entity.tableName) ~ " AS " ~ fromClause.first.sqlAlias);
1189 		for (int i = 1; i < fromClause.length; i++)
1190 		{
1191 			FromClauseItem join = fromClause[i];
1192 			FromClauseItem base = join.base;
1193 			assert(join !is null && base !is null);
1194 			res.appendSpace();
1195 
1196 			assert(join.baseProperty !is null);
1197 			if (join.baseProperty.manyToMany)
1198 			{
1199 				string joinTableAlias = base.sqlAlias ~ join.sqlAlias;
1200 				res.appendSQL(join.joinType == JoinType.LeftJoin ? "LEFT JOIN " : "INNER JOIN ");
1201 
1202 				res.appendSQL(dialect.quoteIfNeeded(
1203 						join.baseProperty.joinTable.tableName) ~ " AS " ~ joinTableAlias);
1204 				res.appendSQL(" ON ");
1205 				res.appendSQL(base.sqlAlias);
1206 				res.appendSQL(".");
1207 				res.appendSQL(dialect.quoteIfNeeded(base.entity.getKeyProperty().columnName));
1208 				res.appendSQL("=");
1209 				res.appendSQL(joinTableAlias);
1210 				res.appendSQL(".");
1211 				res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.joinTable.column1));
1212 
1213 				res.appendSpace();
1214 
1215 				res.appendSQL(join.joinType == JoinType.LeftJoin ? "LEFT JOIN " : "INNER JOIN ");
1216 				res.appendSQL(dialect.quoteIfNeeded(join.entity.tableName) ~ " AS " ~ join.sqlAlias);
1217 				res.appendSQL(" ON ");
1218 				res.appendSQL(joinTableAlias);
1219 				res.appendSQL(".");
1220 				res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.joinTable.column2));
1221 				res.appendSQL("=");
1222 				res.appendSQL(join.sqlAlias);
1223 				res.appendSQL(".");
1224 				res.appendSQL(dialect.quoteIfNeeded(join.entity.getKeyProperty().columnName));
1225 			}
1226 			else
1227 			{
1228 				res.appendSQL(join.joinType == JoinType.LeftJoin ? "LEFT JOIN " : "INNER JOIN ");
1229 				res.appendSQL(dialect.quoteIfNeeded(join.entity.tableName) ~ " AS " ~ join.sqlAlias);
1230 				res.appendSQL(" ON ");
1231 				//writeln("adding ON");
1232 				if (join.baseProperty.oneToOne)
1233 				{
1234 					assert(join.baseProperty.columnName !is null
1235 							|| join.baseProperty.referencedProperty !is null);
1236 					if (join.baseProperty.columnName !is null)
1237 					{
1238 						//writeln("fk is in base");
1239 						res.appendSQL(base.sqlAlias);
1240 						res.appendSQL(".");
1241 						res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.columnName));
1242 						res.appendSQL("=");
1243 						res.appendSQL(join.sqlAlias);
1244 						res.appendSQL(".");
1245 						res.appendSQL(dialect.quoteIfNeeded(join.entity.getKeyProperty()
1246 								.columnName));
1247 					}
1248 					else
1249 					{
1250 						//writeln("fk is in join");
1251 						res.appendSQL(base.sqlAlias);
1252 						res.appendSQL(".");
1253 						res.appendSQL(dialect.quoteIfNeeded(base.entity.getKeyProperty()
1254 								.columnName));
1255 						res.appendSQL("=");
1256 						res.appendSQL(join.sqlAlias);
1257 						res.appendSQL(".");
1258 						res.appendSQL(dialect.quoteIfNeeded(
1259 								join.baseProperty.referencedProperty.columnName));
1260 					}
1261 				}
1262 				else if (join.baseProperty.manyToOne)
1263 				{
1264 					assert(join.baseProperty.columnName !is null,
1265 							"ManyToOne should have JoinColumn as well");
1266 					//writeln("fk is in base");
1267 					res.appendSQL(base.sqlAlias);
1268 					res.appendSQL(".");
1269 					res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.columnName));
1270 					res.appendSQL("=");
1271 					res.appendSQL(join.sqlAlias);
1272 					res.appendSQL(".");
1273 					res.appendSQL(dialect.quoteIfNeeded(join.entity.getKeyProperty().columnName));
1274 				}
1275 				else if (join.baseProperty.oneToMany)
1276 				{
1277 					res.appendSQL(base.sqlAlias);
1278 					res.appendSQL(".");
1279 					res.appendSQL(dialect.quoteIfNeeded(base.entity.getKeyProperty().columnName));
1280 					res.appendSQL("=");
1281 					res.appendSQL(join.sqlAlias);
1282 					res.appendSQL(".");
1283 					res.appendSQL(dialect.quoteIfNeeded(
1284 							join.baseProperty.referencedProperty.columnName));
1285 				}
1286 				else
1287 				{
1288 					// TODO: support other relations
1289 					throw new QuerySyntaxException("Invalid relation type in join");
1290 				}
1291 			}
1292 		}
1293 	}
1294 
1295 	void addWhereCondition(Token t, int basePrecedency, Dialect dialect, ParsedQuery res)
1296 	{
1297 		if (t.type == TokenType.Expression)
1298 		{
1299 			addWhereCondition(t.children[0], basePrecedency, dialect, res);
1300 		}
1301 		else if (t.type == TokenType.Field)
1302 		{
1303 			string tableName = t.from.sqlAlias;
1304 			string fieldName = t.field.columnName;
1305 			res.appendSpace();
1306 			res.appendSQL(tableName ~ "." ~ dialect.quoteIfNeeded(fieldName));
1307 		}
1308 		else if (t.type == TokenType.Number)
1309 		{
1310 			res.appendSpace();
1311 			res.appendSQL(t.text);
1312 		}
1313 		else if (t.type == TokenType.String)
1314 		{
1315 			res.appendSpace();
1316 			res.appendSQL(dialect.quoteSqlString(t.text));
1317 		}
1318 		else if (t.type == TokenType.Parameter)
1319 		{
1320 			res.appendSpace();
1321 			res.appendSQL("?");
1322 			res.addParam(t.text);
1323 		}
1324 		else if (t.type == TokenType.CommaDelimitedList)
1325 		{
1326 			bool first = true;
1327 			for (int i = 0; i < t.children.length; i++)
1328 			{
1329 				if (!first)
1330 					res.appendSQL(", ");
1331 				else
1332 					first = false;
1333 				addWhereCondition(t.children[i], 0, dialect, res);
1334 			}
1335 		}
1336 		else if (t.type == TokenType.OpExpr)
1337 		{
1338 			int currentPrecedency = operatorPrecedency(t.operator);
1339 			bool needBraces = currentPrecedency < basePrecedency;
1340 			if (needBraces)
1341 				res.appendSQL("(");
1342 			switch (t.operator)
1343 			{
1344 			case OperatorType.LIKE:
1345 			case OperatorType.EQ:
1346 			case OperatorType.NE:
1347 			case OperatorType.LT:
1348 			case OperatorType.GT:
1349 			case OperatorType.LE:
1350 			case OperatorType.GE:
1351 			case OperatorType.MUL:
1352 			case OperatorType.ADD:
1353 			case OperatorType.SUB:
1354 			case OperatorType.DIV:
1355 			case OperatorType.AND:
1356 			case OperatorType.OR:
1357 			case OperatorType.IDIV:
1358 			case OperatorType.MOD:
1359 				// binary op
1360 				if (!needBraces)
1361 					res.appendSpace();
1362 				addWhereCondition(t.children[0], currentPrecedency, dialect, res);
1363 				res.appendSpace();
1364 				res.appendSQL(t.text);
1365 				res.appendSpace();
1366 				addWhereCondition(t.children[1], currentPrecedency, dialect, res);
1367 				break;
1368 			case OperatorType.UNARY_PLUS:
1369 			case OperatorType.UNARY_MINUS:
1370 			case OperatorType.NOT:
1371 				// unary op
1372 				if (!needBraces)
1373 					res.appendSpace();
1374 				res.appendSQL(t.text);
1375 				res.appendSpace();
1376 				addWhereCondition(t.children[0], currentPrecedency, dialect, res);
1377 				break;
1378 			case OperatorType.IS_NULL:
1379 			case OperatorType.IS_NOT_NULL:
1380 				addWhereCondition(t.children[0], currentPrecedency, dialect, res);
1381 				res.appendSpace();
1382 				res.appendSQL(t.text);
1383 				res.appendSpace();
1384 				break;
1385 			case OperatorType.BETWEEN:
1386 				if (!needBraces)
1387 					res.appendSpace();
1388 				addWhereCondition(t.children[0], currentPrecedency, dialect, res);
1389 				res.appendSQL(" BETWEEN ");
1390 				addWhereCondition(t.children[1], currentPrecedency, dialect, res);
1391 				res.appendSQL(" AND ");
1392 				addWhereCondition(t.children[2], currentPrecedency, dialect, res);
1393 				break;
1394 			case OperatorType.IN:
1395 				if (!needBraces)
1396 					res.appendSpace();
1397 				addWhereCondition(t.children[0], currentPrecedency, dialect, res);
1398 				res.appendSQL(" IN (");
1399 				addWhereCondition(t.children[1], currentPrecedency, dialect, res);
1400 				res.appendSQL(")");
1401 				break;
1402 			case OperatorType.IS:
1403 			default:
1404 				enforceHelper!QuerySyntaxException(false,
1405 						"Unexpected operator" ~ errorContext(t));
1406 				break;
1407 			}
1408 			if (needBraces)
1409 				res.appendSQL(")");
1410 		}
1411 	}
1412 
1413 	void addWhereSQL(Dialect dialect, ParsedQuery res)
1414 	{
1415 		if (whereClause is null)
1416 			return;
1417 		res.appendSpace();
1418 		res.appendSQL("WHERE ");
1419 		addWhereCondition(whereClause, 0, dialect, res);
1420 	}
1421 
1422 	void addOrderBySQL(Dialect dialect, ParsedQuery res)
1423 	{
1424 		if (orderByClause.length == 0)
1425 			return;
1426 		res.appendSpace();
1427 		res.appendSQL("ORDER BY ");
1428 		bool first = true;
1429 		// individual fields specified
1430 		foreach (a; orderByClause)
1431 		{
1432 			string fieldName = a.prop.columnName;
1433 			string tableName = a.from.sqlAlias;
1434 			if (!first)
1435 			{
1436 				res.appendSQL(", ");
1437 			}
1438 			else
1439 				first = false;
1440 			res.appendSQL(tableName ~ "." ~ dialect.quoteIfNeeded(fieldName));
1441 			if (!a.asc)
1442 				res.appendSQL(" DESC");
1443 		}
1444 	}
1445 
1446 	ParsedQuery makeSQL(Dialect dialect)
1447 	{
1448 		ParsedQuery res = new ParsedQuery(query);
1449 		addSelectSQL(dialect, res);
1450 		addFromSQL(dialect, res);
1451 		addWhereSQL(dialect, res);
1452 		addOrderBySQL(dialect, res);
1453 		return res;
1454 	}
1455 
1456 }
1457 
1458 enum KeywordType
1459 {
1460 	NONE,
1461 	SELECT,
1462 	FROM,
1463 	WHERE,
1464 	ORDER,
1465 	BY,
1466 	ASC,
1467 	DESC,
1468 	JOIN,
1469 	INNER,
1470 	OUTER,
1471 	LEFT,
1472 	RIGHT,
1473 	FETCH,
1474 	AS,
1475 	LIKE,
1476 	IN,
1477 	IS,
1478 	NOT,
1479 	NULL,
1480 	AND,
1481 	OR,
1482 	BETWEEN,
1483 	DIV,
1484 	MOD,
1485 }
1486 
1487 KeywordType isKeyword(string str)
1488 {
1489 	return isKeyword(str.dup);
1490 }
1491 
1492 KeywordType isKeyword(char[] str)
1493 {
1494 	char[] s = toUpper(str);
1495 	if (s == "SELECT")
1496 		return KeywordType.SELECT;
1497 	if (s == "FROM")
1498 		return KeywordType.FROM;
1499 	if (s == "WHERE")
1500 		return KeywordType.WHERE;
1501 	if (s == "ORDER")
1502 		return KeywordType.ORDER;
1503 	if (s == "BY")
1504 		return KeywordType.BY;
1505 	if (s == "ASC")
1506 		return KeywordType.ASC;
1507 	if (s == "DESC")
1508 		return KeywordType.DESC;
1509 	if (s == "JOIN")
1510 		return KeywordType.JOIN;
1511 	if (s == "INNER")
1512 		return KeywordType.INNER;
1513 	if (s == "OUTER")
1514 		return KeywordType.OUTER;
1515 	if (s == "LEFT")
1516 		return KeywordType.LEFT;
1517 	if (s == "RIGHT")
1518 		return KeywordType.RIGHT;
1519 	if (s == "FETCH")
1520 		return KeywordType.FETCH;
1521 	if (s == "LIKE")
1522 		return KeywordType.LIKE;
1523 	if (s == "IN")
1524 		return KeywordType.IN;
1525 	if (s == "IS")
1526 		return KeywordType.IS;
1527 	if (s == "NOT")
1528 		return KeywordType.NOT;
1529 	if (s == "NULL")
1530 		return KeywordType.NULL;
1531 	if (s == "AS")
1532 		return KeywordType.AS;
1533 	if (s == "AND")
1534 		return KeywordType.AND;
1535 	if (s == "OR")
1536 		return KeywordType.OR;
1537 	if (s == "BETWEEN")
1538 		return KeywordType.BETWEEN;
1539 	if (s == "DIV")
1540 		return KeywordType.DIV;
1541 	if (s == "MOD")
1542 		return KeywordType.MOD;
1543 	return KeywordType.NONE;
1544 }
1545 
1546 unittest
1547 {
1548 	assert(isKeyword("Null") == KeywordType.NULL);
1549 	assert(isKeyword("from") == KeywordType.FROM);
1550 	assert(isKeyword("SELECT") == KeywordType.SELECT);
1551 	assert(isKeyword("blabla") == KeywordType.NONE);
1552 }
1553 
1554 enum OperatorType
1555 {
1556 	NONE,
1557 
1558 	// symbolic
1559 	EQ, // ==
1560 	NE, // != <>
1561 	LT, // <
1562 	GT, // >
1563 	LE, // <=
1564 	GE, // >=
1565 	MUL, // *
1566 	ADD, // +
1567 	SUB, // -
1568 	DIV, // /
1569 
1570 	// from keywords
1571 	LIKE,
1572 	IN,
1573 	IS,
1574 	NOT,
1575 	AND,
1576 	OR,
1577 	BETWEEN,
1578 	IDIV,
1579 	MOD,
1580 
1581 	UNARY_PLUS,
1582 	UNARY_MINUS,
1583 
1584 	IS_NULL,
1585 	IS_NOT_NULL,
1586 }
1587 
1588 OperatorType isOperator(KeywordType t)
1589 {
1590 	switch (t)
1591 	{
1592 	case KeywordType.LIKE:
1593 		return OperatorType.LIKE;
1594 	case KeywordType.IN:
1595 		return OperatorType.IN;
1596 	case KeywordType.IS:
1597 		return OperatorType.IS;
1598 	case KeywordType.NOT:
1599 		return OperatorType.NOT;
1600 	case KeywordType.AND:
1601 		return OperatorType.AND;
1602 	case KeywordType.OR:
1603 		return OperatorType.OR;
1604 	case KeywordType.BETWEEN:
1605 		return OperatorType.BETWEEN;
1606 	case KeywordType.DIV:
1607 		return OperatorType.IDIV;
1608 	case KeywordType.MOD:
1609 		return OperatorType.MOD;
1610 	default:
1611 		return OperatorType.NONE;
1612 	}
1613 }
1614 
1615 int operatorPrecedency(OperatorType t)
1616 {
1617 	switch (t)
1618 	{
1619 	case OperatorType.EQ:
1620 		return 5; // ==
1621 	case OperatorType.NE:
1622 		return 5; // != <>
1623 	case OperatorType.LT:
1624 		return 5; // <
1625 	case OperatorType.GT:
1626 		return 5; // >
1627 	case OperatorType.LE:
1628 		return 5; // <=
1629 	case OperatorType.GE:
1630 		return 5; // >=
1631 	case OperatorType.MUL:
1632 		return 10; // *
1633 	case OperatorType.ADD:
1634 		return 9; // +
1635 	case OperatorType.SUB:
1636 		return 9; // -
1637 	case OperatorType.DIV:
1638 		return 10; // /
1639 		// from keywords
1640 	case OperatorType.LIKE:
1641 		return 11;
1642 	case OperatorType.IN:
1643 		return 12;
1644 	case OperatorType.IS:
1645 		return 13;
1646 	case OperatorType.NOT:
1647 		return 6; // ???
1648 	case OperatorType.AND:
1649 		return 4;
1650 	case OperatorType.OR:
1651 		return 3;
1652 	case OperatorType.BETWEEN:
1653 		return 7; // ???
1654 	case OperatorType.IDIV:
1655 		return 10;
1656 	case OperatorType.MOD:
1657 		return 10;
1658 	case OperatorType.UNARY_PLUS:
1659 		return 15;
1660 	case OperatorType.UNARY_MINUS:
1661 		return 15;
1662 	case OperatorType.IS_NULL:
1663 		return 15;
1664 	case OperatorType.IS_NOT_NULL:
1665 		return 15;
1666 	default:
1667 		return -1;
1668 	}
1669 }
1670 
1671 OperatorType isOperator(string s, ref int i)
1672 {
1673 	int len = cast(int) s.length;
1674 	char ch = s[i];
1675 	char ch2 = i < len - 1 ? s[i + 1] : 0;
1676 	//char ch3 = i < len - 2 ? s[i + 2] : 0;
1677 	if (ch == '=' && ch2 == '=')
1678 	{
1679 		i++;
1680 		return OperatorType.EQ;
1681 	} // ==
1682 	if (ch == '!' && ch2 == '=')
1683 	{
1684 		i++;
1685 		return OperatorType.NE;
1686 	} // !=
1687 	if (ch == '<' && ch2 == '>')
1688 	{
1689 		i++;
1690 		return OperatorType.NE;
1691 	} // <>
1692 	if (ch == '<' && ch2 == '=')
1693 	{
1694 		i++;
1695 		return OperatorType.LE;
1696 	} // <=
1697 	if (ch == '>' && ch2 == '=')
1698 	{
1699 		i++;
1700 		return OperatorType.GE;
1701 	} // >=
1702 	if (ch == '=')
1703 		return OperatorType.EQ; // =
1704 	if (ch == '<')
1705 		return OperatorType.LT; // <
1706 	if (ch == '>')
1707 		return OperatorType.GT; // <
1708 	if (ch == '*')
1709 		return OperatorType.MUL; // <
1710 	if (ch == '+')
1711 		return OperatorType.ADD; // <
1712 	if (ch == '-')
1713 		return OperatorType.SUB; // <
1714 	if (ch == '/')
1715 		return OperatorType.DIV; // <
1716 	return OperatorType.NONE;
1717 }
1718 
1719 enum TokenType
1720 {
1721 	Keyword, // WHERE
1722 	Ident, // ident
1723 	Number, // 25   13.5e-10
1724 	String, // 'string'
1725 	Operator, // == != <= >= < > + - * /
1726 	Dot, // .
1727 	OpenBracket, // (
1728 	CloseBracket, // )
1729 	Comma, // ,
1730 	Entity, // entity name
1731 	Field, // field name of some entity
1732 	Alias, // alias name of some entity
1733 	Parameter, // ident after :
1734 	// types of compound AST nodes
1735 	Expression, // any expression
1736 	Braces, // ( tokens )
1737 	CommaDelimitedList, // tokens, ... , tokens
1738 	OpExpr, // operator expression; current token == operator, children = params
1739 }
1740 
1741 class Token
1742 {
1743 	int pos;
1744 	TokenType type;
1745 	KeywordType keyword = KeywordType.NONE;
1746 	OperatorType operator = OperatorType.NONE;
1747 	string text;
1748 	string spaceAfter;
1749 	EntityInfo entity;
1750 	PropertyInfo field;
1751 	FromClauseItem from;
1752 	Token[] children;
1753 	this(int pos, TokenType type, string text)
1754 	{
1755 		this.pos = pos;
1756 		this.type = type;
1757 		this.text = text;
1758 	}
1759 
1760 	this(int pos, KeywordType keyword, string text)
1761 	{
1762 		this.pos = pos;
1763 		this.type = TokenType.Keyword;
1764 		this.keyword = keyword;
1765 		this.text = text;
1766 	}
1767 
1768 	this(int pos, OperatorType op, string text)
1769 	{
1770 		this.pos = pos;
1771 		this.type = TokenType.Operator;
1772 		this.operator = op;
1773 		this.text = text;
1774 	}
1775 
1776 	this(int pos, TokenType type, Token[] base, int start, int end)
1777 	{
1778 		this.pos = pos;
1779 		this.type = type;
1780 		this.children = new Token[end - start];
1781 		for (int i = start; i < end; i++)
1782 			children[i - start] = base[i];
1783 	}
1784 	// unary operator expression
1785 	this(int pos, OperatorType type, string text, Token right)
1786 	{
1787 		this.pos = pos;
1788 		this.type = TokenType.OpExpr;
1789 		this.operator = type;
1790 		this.text = text;
1791 		this.children = new Token[1];
1792 		this.children[0] = right;
1793 	}
1794 	// binary operator expression
1795 	this(int pos, OperatorType type, string text, Token left, Token right)
1796 	{
1797 		this.pos = pos;
1798 		this.type = TokenType.OpExpr;
1799 		this.text = text;
1800 		this.operator = type;
1801 		this.children = new Token[2];
1802 		this.children[0] = left;
1803 		this.children[1] = right;
1804 	}
1805 
1806 	bool isExpression()
1807 	{
1808 		return type == TokenType.Expression || type == TokenType.Braces || type == TokenType.OpExpr
1809 			|| type == TokenType.Parameter || type == TokenType.Field
1810 			|| type == TokenType.String || type == TokenType.Number;
1811 	}
1812 
1813 	bool isCompound()
1814 	{
1815 		return this.type >= TokenType.Expression;
1816 	}
1817 
1818 	string dump(int level)
1819 	{
1820 		string res;
1821 		for (int i = 0; i < level; i++)
1822 			res ~= "    ";
1823 		res ~= toString() ~ "\n";
1824 		foreach (c; children)
1825 			res ~= c.dump(level + 1);
1826 		return res;
1827 	}
1828 
1829 	override string toString()
1830 	{
1831 		switch (type)
1832 		{
1833 		case TokenType.Keyword: // WHERE
1834 		case TokenType.Ident:
1835 			return "`" ~ text ~ "`"; // ident
1836 		case TokenType.Number:
1837 			return "" ~ text; // 25   13.5e-10
1838 		case TokenType.String:
1839 			return "'" ~ text ~ "'"; // 'string'
1840 		case TokenType.Operator:
1841 			return "op:" ~ text; // == != <= >= < > + - * /
1842 		case TokenType.Dot:
1843 			return "."; // .
1844 		case TokenType.OpenBracket:
1845 			return "("; // (
1846 		case TokenType.CloseBracket:
1847 			return ")"; // )
1848 		case TokenType.Comma:
1849 			return ","; // ,
1850 		case TokenType.Entity:
1851 			return "entity: " ~ entity.name; // entity name
1852 		case TokenType.Field:
1853 			return from.entityAlias ~ "." ~ field.propertyName; // field name of some entity
1854 		case TokenType.Alias:
1855 			return "alias: " ~ text; // alias name of some entity
1856 		case TokenType.Parameter:
1857 			return ":" ~ text; // ident after :
1858 			// types of compound AST nodes
1859 		case TokenType.Expression:
1860 			return "expr"; // any expression
1861 		case TokenType.Braces:
1862 			return "()"; // ( tokens )
1863 		case TokenType.CommaDelimitedList:
1864 			return ",,,"; // tokens, ... , tokens
1865 		case TokenType.OpExpr:
1866 			return "" ~ text;
1867 		default:
1868 			return "UNKNOWN";
1869 		}
1870 	}
1871 
1872 }
1873 
1874 Token[] tokenize(string s)
1875 {
1876 	Token[] res;
1877 	int startpos = 0;
1878 	int state = 0;
1879 	int len = cast(int) s.length;
1880 	for (int i = 0; i < len; i++)
1881 	{
1882 		char ch = s[i];
1883 		char ch2 = i < len - 1 ? s[i + 1] : 0;
1884 		char ch3 = i < len - 2 ? s[i + 2] : 0;
1885 		string text;
1886 		bool quotedIdent = ch == '`';
1887 		startpos = i;
1888 		OperatorType op = isOperator(s, i);
1889 		if (op != OperatorType.NONE)
1890 		{
1891 			// operator
1892 			res ~= new Token(startpos, op, s[startpos .. i + 1]);
1893 		}
1894 		else if (ch == ':' && (isAlpha(ch2) || ch2 == '_'))
1895 		{
1896 			// parameter name
1897 			i++;
1898 			// && state == 0
1899 			for (int j = i; j < len; j++)
1900 			{
1901 				if (isAlphaNum(s[j]) || s[j] == '_')
1902 				{
1903 					text ~= s[j];
1904 					i = j;
1905 				}
1906 				else
1907 				{
1908 					break;
1909 				}
1910 			}
1911 			enforceHelper!QuerySyntaxException(text.length > 0,
1912 					"Invalid parameter name near " ~ cast(string) s[startpos .. $]);
1913 			res ~= new Token(startpos, TokenType.Parameter, text);
1914 		}
1915 		else if (isAlpha(ch) || ch == '_' || quotedIdent)
1916 		{
1917 			// identifier or keyword
1918 			if (quotedIdent)
1919 			{
1920 				i++;
1921 				enforceHelper!QuerySyntaxException(i < len - 1,
1922 						"Invalid quoted identifier near " ~ cast(string) s[startpos .. $]);
1923 			}
1924 			// && state == 0
1925 			for (int j = i; j < len; j++)
1926 			{
1927 				if (isAlphaNum(s[j]) || s[j] == '_')
1928 				{
1929 					text ~= s[j];
1930 					i = j;
1931 				}
1932 				else
1933 				{
1934 					break;
1935 				}
1936 			}
1937 			enforceHelper!QuerySyntaxException(text.length > 0,
1938 					"Invalid quoted identifier near " ~ cast(string) s[startpos .. $]);
1939 			if (quotedIdent)
1940 			{
1941 				enforceHelper!QuerySyntaxException(i < len - 1 && s[i + 1] == '`',
1942 						"Invalid quoted identifier near " ~ cast(string) s[startpos .. $]);
1943 				i++;
1944 			}
1945 			KeywordType keywordId = isKeyword(text);
1946 			if (keywordId != KeywordType.NONE && !quotedIdent)
1947 			{
1948 				OperatorType keywordOp = isOperator(keywordId);
1949 				if (keywordOp != OperatorType.NONE)
1950 					res ~= new Token(startpos, keywordOp, text); // operator keyword
1951 				else
1952 					res ~= new Token(startpos, keywordId, text);
1953 			}
1954 			else
1955 				res ~= new Token(startpos, TokenType.Ident, text);
1956 		}
1957 		else if (isWhite(ch))
1958 		{
1959 			// whitespace
1960 			for (int j = i; j < len; j++)
1961 			{
1962 				if (isWhite(s[j]))
1963 				{
1964 					text ~= s[j];
1965 					i = j;
1966 				}
1967 				else
1968 				{
1969 					break;
1970 				}
1971 			}
1972 			// don't add whitespace to lexer results as separate token
1973 			// add as spaceAfter
1974 			if (res.length > 0)
1975 			{
1976 				res[$ - 1].spaceAfter = text;
1977 			}
1978 		}
1979 		else if (ch == '\'')
1980 		{
1981 			// string constant
1982 			i++;
1983 			for (int j = i; j < len; j++)
1984 			{
1985 				if (s[j] != '\'')
1986 				{
1987 					text ~= s[j];
1988 					i = j;
1989 				}
1990 				else
1991 				{
1992 					break;
1993 				}
1994 			}
1995 			enforceHelper!QuerySyntaxException(i < len - 1 && s[i + 1] == '\'',
1996 					"Unfinished string near " ~ cast(string) s[startpos .. $]);
1997 			i++;
1998 			res ~= new Token(startpos, TokenType.String, text);
1999 		}
2000 		else if (isDigit(ch) || (ch == '.' && isDigit(ch2)))
2001 		{
2002 			// numeric constant
2003 			if (ch == '.')
2004 			{
2005 				// .25
2006 				text ~= '.';
2007 				i++;
2008 				for (int j = i; j < len; j++)
2009 				{
2010 					if (isDigit(s[j]))
2011 					{
2012 						text ~= s[j];
2013 						i = j;
2014 					}
2015 					else
2016 					{
2017 						break;
2018 					}
2019 				}
2020 			}
2021 			else
2022 			{
2023 				// 123
2024 				for (int j = i; j < len; j++)
2025 				{
2026 					if (isDigit(s[j]))
2027 					{
2028 						text ~= s[j];
2029 						i = j;
2030 					}
2031 					else
2032 					{
2033 						break;
2034 					}
2035 				}
2036 				// .25
2037 				if (i < len - 1 && s[i + 1] == '.')
2038 				{
2039 					text ~= '.';
2040 					i++;
2041 					for (int j = i; j < len; j++)
2042 					{
2043 						if (isDigit(s[j]))
2044 						{
2045 							text ~= s[j];
2046 							i = j;
2047 						}
2048 						else
2049 						{
2050 							break;
2051 						}
2052 					}
2053 				}
2054 			}
2055 			if (i < len - 1 && std.ascii.toLower(s[i + 1]) == 'e')
2056 			{
2057 				text ~= s[i + 1];
2058 				i++;
2059 				if (i < len - 1 && (s[i + 1] == '-' || s[i + 1] == '+'))
2060 				{
2061 					text ~= s[i + 1];
2062 					i++;
2063 				}
2064 				enforceHelper!QuerySyntaxException(i < len - 1 && isDigit(s[i]),
2065 						"Invalid number near " ~ cast(string) s[startpos .. $]);
2066 				for (int j = i; j < len; j++)
2067 				{
2068 					if (isDigit(s[j]))
2069 					{
2070 						text ~= s[j];
2071 						i = j;
2072 					}
2073 					else
2074 					{
2075 						break;
2076 					}
2077 				}
2078 			}
2079 			enforceHelper!QuerySyntaxException(i >= len - 1 || !isAlpha(s[i]),
2080 					"Invalid number near " ~ cast(string) s[startpos .. $]);
2081 			res ~= new Token(startpos, TokenType.Number, text);
2082 		}
2083 		else if (ch == '.')
2084 		{
2085 			res ~= new Token(startpos, TokenType.Dot, ".");
2086 		}
2087 		else if (ch == '(')
2088 		{
2089 			res ~= new Token(startpos, TokenType.OpenBracket, "(");
2090 		}
2091 		else if (ch == ')')
2092 		{
2093 			res ~= new Token(startpos, TokenType.CloseBracket, ")");
2094 		}
2095 		else if (ch == ',')
2096 		{
2097 			res ~= new Token(startpos, TokenType.Comma, ",");
2098 		}
2099 		else
2100 		{
2101 			enforceHelper!QuerySyntaxException(false,
2102 					"Invalid character near " ~ cast(string) s[startpos .. $]);
2103 		}
2104 	}
2105 	return res;
2106 }
2107 
2108 unittest
2109 {
2110 	Token[] tokens;
2111 	tokens = tokenize(
2112 			"SELECT a From User a where a.flags = 12 AND a.name='john' ORDER BY a.idx ASC");
2113 	assert(tokens.length == 23);
2114 	assert(tokens[0].type == TokenType.Keyword);
2115 	assert(tokens[2].type == TokenType.Keyword);
2116 	assert(tokens[5].type == TokenType.Keyword);
2117 	assert(tokens[5].text == "where");
2118 	assert(tokens[10].type == TokenType.Number);
2119 	assert(tokens[10].text == "12");
2120 	assert(tokens[16].type == TokenType.String);
2121 	assert(tokens[16].text == "john");
2122 	assert(tokens[22].type == TokenType.Keyword);
2123 	assert(tokens[22].text == "ASC");
2124 }
2125 
2126 class ParameterValues
2127 {
2128 	Variant[string] values;
2129 	int[][string] params;
2130 	int[string] unboundParams;
2131 	this(int[][string] params)
2132 	{
2133 		this.params = params;
2134 		foreach (key, value; params)
2135 		{
2136 			unboundParams[key] = 1;
2137 		}
2138 	}
2139 
2140 	void setParameter(string name, Variant value)
2141 	{
2142 		enforceHelper!QueryParameterException((name in params) !is null,
2143 				"Attempting to set unknown parameter " ~ name);
2144 		unboundParams.remove(name);
2145 		values[name] = value;
2146 	}
2147 
2148 	void checkAllParametersSet()
2149 	{
2150 		if (unboundParams.length == 0)
2151 			return;
2152 		string list;
2153 		foreach (key, value; unboundParams)
2154 		{
2155 			if (list.length > 0)
2156 				list ~= ", ";
2157 			list ~= key;
2158 		}
2159 		enforceHelper!QueryParameterException(false, "Parameters " ~ list ~ " not set");
2160 	}
2161 
2162 	void applyParams(DataSetWriter ds)
2163 	{
2164 		foreach (key, indexes; params)
2165 		{
2166 			Variant value = values[key];
2167 			foreach (i; indexes)
2168 				ds.setVariant(i, value);
2169 		}
2170 	}
2171 }
2172 
2173 class ParsedQuery
2174 {
2175 	private string _hql;
2176 	private string _sql;
2177 	private int[][string] params; // contains 1-based indexes of ? ? ? placeholders in SQL for param by name
2178 	private int paramIndex = 1;
2179 	private FromClause _from;
2180 	private SelectClauseItem[] _select;
2181 	private EntityInfo _entity;
2182 	private int _colCount = 0;
2183 	this(string hql)
2184 	{
2185 		_hql = hql;
2186 	}
2187 
2188 	@property string hql()
2189 	{
2190 		return _hql;
2191 	}
2192 
2193 	@property string sql()
2194 	{
2195 		return _sql;
2196 	}
2197 
2198 	@property const(EntityInfo) entity()
2199 	{
2200 		return _entity;
2201 	}
2202 
2203 	@property int colCount()
2204 	{
2205 		return _colCount;
2206 	}
2207 
2208 	@property FromClause from()
2209 	{
2210 		return _from;
2211 	}
2212 
2213 	@property SelectClauseItem[] select()
2214 	{
2215 		return _select;
2216 	}
2217 
2218 	void setEntity(const EntityInfo entity)
2219 	{
2220 		_entity = cast(EntityInfo) entity;
2221 	}
2222 
2223 	void setFromClause(FromClause from)
2224 	{
2225 		_from = from;
2226 	}
2227 
2228 	void setSelect(SelectClauseItem[] items)
2229 	{
2230 		_select = items;
2231 	}
2232 
2233 	void setColCount(int cnt)
2234 	{
2235 		_colCount = cnt;
2236 	}
2237 
2238 	void addParam(string paramName)
2239 	{
2240 		if ((paramName in params) is null)
2241 		{
2242 			params[paramName] = [paramIndex++];
2243 		}
2244 		else
2245 		{
2246 			params[paramName] ~= [paramIndex++];
2247 		}
2248 	}
2249 
2250 	int[] getParam(string paramName)
2251 	{
2252 		if ((paramName in params) is null)
2253 		{
2254 			throw new DStructException("Parameter " ~ paramName ~ " not found in query " ~ _hql);
2255 		}
2256 		else
2257 		{
2258 			return params[paramName];
2259 		}
2260 	}
2261 
2262 	void appendSQL(string sql)
2263 	{
2264 		_sql ~= sql;
2265 	}
2266 
2267 	void appendSpace()
2268 	{
2269 		if (_sql.length > 0 && _sql[$ - 1] != ' ')
2270 			_sql ~= ' ';
2271 	}
2272 
2273 	ParameterValues createParams()
2274 	{
2275 		return new ParameterValues(params);
2276 	}
2277 }
2278 
2279 unittest
2280 {
2281 	ParsedQuery q = new ParsedQuery("FROM User where id = :param1 or id = :param2");
2282 	q.addParam("param1"); // 1
2283 	q.addParam("param2"); // 2
2284 	q.addParam("param1"); // 3
2285 	q.addParam("param1"); // 4
2286 	q.addParam("param3"); // 5
2287 	q.addParam("param2"); // 6
2288 	assert(q.getParam("param1") == [1, 3, 4]);
2289 	assert(q.getParam("param2") == [2, 6]);
2290 	assert(q.getParam("param3") == [5]);
2291 }
2292 
2293 unittest
2294 {
2295 	import dstruct.tests;
2296 
2297 	EntityMetaData schema = new SchemaInfoImpl!(User, Customer, AccountType,
2298 			Address, Person, MoreInfo, EvenMoreInfo, Role);
2299 	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");
2300 	assert(parser.parameterNames.length == 2);
2301 	//writeln("param1=" ~ parser.parameterNames[0]);
2302 	//writeln("param2=" ~ parser.parameterNames[1]);
2303 	assert(parser.parameterNames[0] == "Id");
2304 	assert(parser.parameterNames[1] == "skipName");
2305 	assert(parser.fromClause.length == 1);
2306 	assert(parser.fromClause.first.entity.name == "User");
2307 	assert(parser.fromClause.first.entityAlias == "a");
2308 	assert(parser.selectClause.length == 1);
2309 	assert(parser.selectClause[0].prop is null);
2310 	assert(parser.selectClause[0].from.entity.name == "User");
2311 	assert(parser.orderByClause.length == 2);
2312 	assert(parser.orderByClause[0].prop.propertyName == "name");
2313 	assert(parser.orderByClause[0].from.entity.name == "User");
2314 	assert(parser.orderByClause[0].asc == true);
2315 	assert(parser.orderByClause[1].prop.propertyName == "flags");
2316 	assert(parser.orderByClause[1].from.entity.name == "User");
2317 	assert(parser.orderByClause[1].asc == false);
2318 
2319 	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");
2320 	assert(parser.whereClause !is null);
2321 	//writeln(parser.whereClause.dump(0));
2322 	Dialect dialect = new MySQLDialect();
2323 
2324 	assert(dialect.quoteSqlString("abc") == "'abc'");
2325 	assert(dialect.quoteSqlString("a'b'c") == "'a\\'b\\'c'");
2326 	assert(dialect.quoteSqlString("a\nc") == "'a\\nc'");
2327 
2328 	parser = new QueryParser(schema, "FROM User AS u WHERE id = :Id and u.name like '%test%'");
2329 	ParsedQuery q = parser.makeSQL(dialect);
2330 	//writeln(parser.whereClause.dump(0));
2331 	//writeln(q.hql ~ "\n=>\n" ~ q.sql);
2332 
2333 	//writeln(q.hql);
2334 	//writeln(q.sql);
2335 	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");
2336 	assert(parser.fromClause.hasAlias("a"));
2337 	assert(parser.fromClause.hasAlias("b"));
2338 	assert(parser.fromClause.findByAlias("a").entityName == "Person");
2339 	assert(parser.fromClause.findByAlias("b").entityName == "MoreInfo");
2340 	assert(parser.fromClause.findByAlias("b").joinType == JoinType.LeftJoin);
2341 	assert(parser.fromClause.findByAlias("c").entityName == "EvenMoreInfo");
2342 	// indirect JOIN
2343 	parser = new QueryParser(schema,
2344 			"SELECT a FROM Person a WHERE a.id = :Id AND a.moreInfo.evenMore.flags > 0");
2345 	assert(parser.fromClause.hasAlias("a"));
2346 	assert(parser.fromClause.length == 3);
2347 	assert(parser.fromClause[0].entity.tableName == "person");
2348 	assert(parser.fromClause[1].entity.tableName == "person_info");
2349 	assert(parser.fromClause[1].joinType == JoinType.InnerJoin);
2350 	assert(parser.fromClause[1].pathString == "a.moreInfo");
2351 	assert(parser.fromClause[2].entity.tableName == "person_info2");
2352 	assert(parser.fromClause[2].joinType == JoinType.LeftJoin);
2353 	assert(parser.fromClause[2].pathString == "a.moreInfo.evenMore");
2354 	// indirect JOIN, no alias
2355 	parser = new QueryParser(schema, "FROM Person WHERE id = :Id AND moreInfo.evenMore.flags > 0");
2356 	assert(parser.fromClause.length == 3);
2357 	assert(parser.fromClause[0].entity.tableName == "person");
2358 	assert(parser.fromClause[0].fetch == true);
2359 	//writeln("select fields [" ~ to!string(parser.fromClause[0].startColumn) ~ ", " ~ to!string(parser.fromClause[0].selectedColumns) ~ "]");
2360 	//writeln("select fields [" ~ to!string(parser.fromClause[1].startColumn) ~ ", " ~ to!string(parser.fromClause[1].selectedColumns) ~ "]");
2361 	//writeln("select fields [" ~ to!string(parser.fromClause[2].startColumn) ~ ", " ~ to!string(parser.fromClause[2].selectedColumns) ~ "]");
2362 	assert(parser.fromClause[0].selectedColumns == 4);
2363 	assert(parser.fromClause[1].entity.tableName == "person_info");
2364 	assert(parser.fromClause[1].joinType == JoinType.InnerJoin);
2365 	assert(parser.fromClause[1].pathString == "_a1.moreInfo");
2366 	assert(parser.fromClause[1].fetch == true);
2367 	assert(parser.fromClause[1].selectedColumns == 2);
2368 	assert(parser.fromClause[2].entity.tableName == "person_info2");
2369 	assert(parser.fromClause[2].joinType == JoinType.LeftJoin);
2370 	assert(parser.fromClause[2].pathString == "_a1.moreInfo.evenMore");
2371 	assert(parser.fromClause[2].fetch == true);
2372 	assert(parser.fromClause[2].selectedColumns == 3);
2373 
2374 	q = parser.makeSQL(dialect);
2375 	//writeln(q.hql);
2376 	//writeln(q.sql);
2377 
2378 	parser = new QueryParser(schema, "FROM User WHERE id in (1, 2, (3 - 1 * 25) / 2, 4 + :Id, 5)");
2379 	//writeln(parser.whereClause.dump(0));
2380 	q = parser.makeSQL(dialect);
2381 	//writeln(q.hql);
2382 	//writeln(q.sql);
2383 
2384 	parser = new QueryParser(schema, "FROM Customer WHERE users.id = 1");
2385 	q = parser.makeSQL(dialect);
2386 	//    writeln(q.hql);
2387 	//    writeln(q.sql);
2388 	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");
2389 
2390 	parser = new QueryParser(schema, "FROM Customer WHERE id = 1");
2391 	q = parser.makeSQL(dialect);
2392 	//    writeln(q.hql);
2393 	//    writeln(q.sql);
2394 	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");
2395 
2396 	parser = new QueryParser(schema, "FROM User WHERE roles.id = 1");
2397 	q = parser.makeSQL(dialect);
2398 	//writeln(q.hql);
2399 	//writeln(q.sql);
2400 	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");
2401 
2402 	parser = new QueryParser(schema, "FROM Role WHERE users.id = 1");
2403 	q = parser.makeSQL(dialect);
2404 	//    writeln(q.hql);
2405 	//    writeln(q.sql);
2406 	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");
2407 
2408 	parser = new QueryParser(schema, "FROM User WHERE customer.id = 1");
2409 	q = parser.makeSQL(dialect);
2410 	//    writeln(q.hql);
2411 	//    writeln(q.sql);
2412 	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");
2413 
2414 	parser = new QueryParser(schema,
2415 			"SELECT a2 FROM User AS a1 JOIN a1.roles AS a2 WHERE a1.id = 1");
2416 	q = parser.makeSQL(dialect);
2417 	//writeln(q.hql);
2418 	//writeln(q.sql);
2419 	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");
2420 
2421 	parser = new QueryParser(schema,
2422 			"SELECT a2 FROM Customer AS a1 JOIN a1.users AS a2 WHERE a1.id = 1");
2423 	q = parser.makeSQL(dialect);
2424 	//writeln(q.hql);
2425 	//writeln(q.sql);
2426 	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");
2427 
2428 }