1 /** 2 * HibernateD - 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 hibernated/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 hibernated.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 ddbc.core; 27 28 import hibernated.annotations; 29 import hibernated.metadata; 30 import hibernated.type; 31 import hibernated.core; 32 import hibernated.dialect; 33 import hibernated.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!HibernatedException(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 HibernatedException("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 1685 //writeln("query unittest"); 1686 import hibernated.tests; 1687 1688 EntityMetaData schema = new SchemaInfoImpl!(User, Customer, AccountType, Address, Person, MoreInfo, EvenMoreInfo, Role); 1689 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"); 1690 assert(parser.parameterNames.length == 2); 1691 //writeln("param1=" ~ parser.parameterNames[0]); 1692 //writeln("param2=" ~ parser.parameterNames[1]); 1693 assert(parser.parameterNames[0] == "Id"); 1694 assert(parser.parameterNames[1] == "skipName"); 1695 assert(parser.fromClause.length == 1); 1696 assert(parser.fromClause.first.entity.name == "User"); 1697 assert(parser.fromClause.first.entityAlias == "a"); 1698 assert(parser.selectClause.length == 1); 1699 assert(parser.selectClause[0].prop is null); 1700 assert(parser.selectClause[0].from.entity.name == "User"); 1701 assert(parser.orderByClause.length == 2); 1702 assert(parser.orderByClause[0].prop.propertyName == "name"); 1703 assert(parser.orderByClause[0].from.entity.name == "User"); 1704 assert(parser.orderByClause[0].asc == true); 1705 assert(parser.orderByClause[1].prop.propertyName == "flags"); 1706 assert(parser.orderByClause[1].from.entity.name == "User"); 1707 assert(parser.orderByClause[1].asc == false); 1708 1709 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"); 1710 assert(parser.whereClause !is null); 1711 //writeln(parser.whereClause.dump(0)); 1712 Dialect dialect = new MySQLDialect(); 1713 1714 assert(dialect.quoteSqlString("abc") == "'abc'"); 1715 assert(dialect.quoteSqlString("a'b'c") == "'a\\'b\\'c'"); 1716 assert(dialect.quoteSqlString("a\nc") == "'a\\nc'"); 1717 1718 parser = new QueryParser(schema, "FROM User AS u WHERE id = :Id and u.name like '%test%'"); 1719 ParsedQuery q = parser.makeSQL(dialect); 1720 //writeln(parser.whereClause.dump(0)); 1721 //writeln(q.hql ~ "\n=>\n" ~ q.sql); 1722 1723 //writeln(q.hql); 1724 //writeln(q.sql); 1725 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"); 1726 assert(parser.fromClause.hasAlias("a")); 1727 assert(parser.fromClause.hasAlias("b")); 1728 assert(parser.fromClause.findByAlias("a").entityName == "Person"); 1729 assert(parser.fromClause.findByAlias("b").entityName == "MoreInfo"); 1730 assert(parser.fromClause.findByAlias("b").joinType == JoinType.LeftJoin); 1731 assert(parser.fromClause.findByAlias("c").entityName == "EvenMoreInfo"); 1732 // indirect JOIN 1733 parser = new QueryParser(schema, "SELECT a FROM Person a WHERE a.id = :Id AND a.moreInfo.evenMore.flags > 0"); 1734 assert(parser.fromClause.hasAlias("a")); 1735 assert(parser.fromClause.length == 3); 1736 assert(parser.fromClause[0].entity.tableName == "person"); 1737 assert(parser.fromClause[1].entity.tableName == "person_info"); 1738 assert(parser.fromClause[1].joinType == JoinType.InnerJoin); 1739 assert(parser.fromClause[1].pathString == "a.moreInfo"); 1740 assert(parser.fromClause[2].entity.tableName == "person_info2"); 1741 assert(parser.fromClause[2].joinType == JoinType.LeftJoin); 1742 assert(parser.fromClause[2].pathString == "a.moreInfo.evenMore"); 1743 // indirect JOIN, no alias 1744 parser = new QueryParser(schema, "FROM Person WHERE id = :Id AND moreInfo.evenMore.flags > 0"); 1745 assert(parser.fromClause.length == 3); 1746 assert(parser.fromClause[0].entity.tableName == "person"); 1747 assert(parser.fromClause[0].fetch == true); 1748 //writeln("select fields [" ~ to!string(parser.fromClause[0].startColumn) ~ ", " ~ to!string(parser.fromClause[0].selectedColumns) ~ "]"); 1749 //writeln("select fields [" ~ to!string(parser.fromClause[1].startColumn) ~ ", " ~ to!string(parser.fromClause[1].selectedColumns) ~ "]"); 1750 //writeln("select fields [" ~ to!string(parser.fromClause[2].startColumn) ~ ", " ~ to!string(parser.fromClause[2].selectedColumns) ~ "]"); 1751 assert(parser.fromClause[0].selectedColumns == 4); 1752 assert(parser.fromClause[1].entity.tableName == "person_info"); 1753 assert(parser.fromClause[1].joinType == JoinType.InnerJoin); 1754 assert(parser.fromClause[1].pathString == "_a1.moreInfo"); 1755 assert(parser.fromClause[1].fetch == true); 1756 assert(parser.fromClause[1].selectedColumns == 2); 1757 assert(parser.fromClause[2].entity.tableName == "person_info2"); 1758 assert(parser.fromClause[2].joinType == JoinType.LeftJoin); 1759 assert(parser.fromClause[2].pathString == "_a1.moreInfo.evenMore"); 1760 assert(parser.fromClause[2].fetch == true); 1761 assert(parser.fromClause[2].selectedColumns == 3); 1762 1763 q = parser.makeSQL(dialect); 1764 //writeln(q.hql); 1765 //writeln(q.sql); 1766 1767 parser = new QueryParser(schema, "FROM User WHERE id in (1, 2, (3 - 1 * 25) / 2, 4 + :Id, 5)"); 1768 //writeln(parser.whereClause.dump(0)); 1769 q = parser.makeSQL(dialect); 1770 //writeln(q.hql); 1771 //writeln(q.sql); 1772 1773 parser = new QueryParser(schema, "FROM Customer WHERE users.id = 1"); 1774 q = parser.makeSQL(dialect); 1775 // writeln(q.hql); 1776 // writeln(q.sql); 1777 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"); 1778 1779 parser = new QueryParser(schema, "FROM Customer WHERE id = 1"); 1780 q = parser.makeSQL(dialect); 1781 // writeln(q.hql); 1782 // writeln(q.sql); 1783 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"); 1784 1785 parser = new QueryParser(schema, "FROM User WHERE roles.id = 1"); 1786 q = parser.makeSQL(dialect); 1787 //writeln(q.hql); 1788 //writeln(q.sql); 1789 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"); 1790 1791 parser = new QueryParser(schema, "FROM Role WHERE users.id = 1"); 1792 q = parser.makeSQL(dialect); 1793 // writeln(q.hql); 1794 // writeln(q.sql); 1795 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"); 1796 1797 parser = new QueryParser(schema, "FROM User WHERE customer.id = 1"); 1798 q = parser.makeSQL(dialect); 1799 // writeln(q.hql); 1800 // writeln(q.sql); 1801 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"); 1802 1803 parser = new QueryParser(schema, "SELECT a2 FROM User AS a1 JOIN a1.roles AS a2 WHERE a1.id = 1"); 1804 q = parser.makeSQL(dialect); 1805 //writeln(q.hql); 1806 //writeln(q.sql); 1807 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"); 1808 1809 parser = new QueryParser(schema, "SELECT a2 FROM Customer AS a1 JOIN a1.users AS a2 WHERE a1.id = 1"); 1810 q = parser.makeSQL(dialect); 1811 //writeln(q.hql); 1812 //writeln(q.sql); 1813 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"); 1814 1815 }