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 }