diff --git a/dub.json b/dub.json index b2d1b78..95f77c0 100644 --- a/dub.json +++ b/dub.json @@ -5,7 +5,8 @@ "homepage": "https://github.com/buggins/hibernated", "license": "BSL-1.0", "dependencies": { - "ddbc": "~>0.6.0" + "ddbc": "~>0.6.0", + "pegged": "~>0.4.9" }, "targetType": "staticLibrary", "targetPath": "lib", diff --git a/source/hibernated/hql.d b/source/hibernated/hql.d new file mode 100644 index 0000000..3c5d0c7 --- /dev/null +++ b/source/hibernated/hql.d @@ -0,0 +1,296 @@ +module hibernated.hql; + +import pegged.grammar; + +mixin(grammar(` +# Basic PEG syntax: https://bford.info/pub/lang/peg.pdf +# Notes about extended PEG syntax: https://github.com/PhilippeSigaud/Pegged/wiki/Extended-PEG-Syntax +# 'txt' = Literal text to match against. +# [a-z] = A character class to match against. +# elem? = Matches an element occurring 0 or 1 times. +# elem* = Matches an element occurring 0 or more times. +# elem+ = Matches an element occurring 1 or more times. +# elem1 / elem2 = First attempts to match elem1, then elem2. +# :elem = Drop the element and its contents from the parse tree. +# &elem = Matches if elem is found, without including elem in the containing rule. +# !elem = Matches if elem is NOT found, without including elem in the containing rule. +HQL: + # Query <- ( SelectQuery / DeleteQuery / UpdateQuery ) eoi + Query <- ( SelectQuery / DeleteQuery ) eoi + SubQuery <- SelectQuery + + SelectQuery <- (SelectClause :spaces)? FromClause (:spaces WhereClause)? (:spaces OrderClause)? + + DeleteQuery <- DeleteKw :spaces FromClause2 (:spaces WhereClause)? + + # Inserts are done by periodically flushing the cache, and shoud be implemented accordingly. + # See: https://docs.jboss.org/hibernate/orm/3.3/reference/en/html/batch.html#batch-inserts + + # The select clause has array, map, and object forms. + # See: https://docs.jboss.org/hibernate/orm/3.3/reference/en/html/queryhql.html#queryhql-select + SelectClause <- :SelectKw :spaces ( MapItems / ObjectItems / ArrayItems ) + MapItems <- :'new' :spaces :'map' :spaces? :'(' :spaces? ArrayItems :spaces? :')' + ObjectItems <- :'new' :spaces IdentifierItem :spaces? :'(' :spaces? ArrayItems :spaces? :')' + ArrayItems <- SelectItem (:spaces? ',' :spaces? SelectItem)* + + SelectItem <- Expression (:spaces Alias)? + Alias <- :(AsKw :spaces)? Identifier + + FromClause <- :FromKw :spaces FromItem + # A variant of the From clause where the FromKw is optional. + FromClause2 <- (:FromKw :spaces)? FromItem + + # See https://www.postgresql.org/docs/16/sql-select.html + # See https://docs.jboss.org/hibernate/orm/3.3/reference/en/html/queryhql.html#queryhql-joins + FromItem <- IdentifierItem (:spaces Alias)? (:spaces JoinItems)? + JoinItems <- JoinItem (:spaces JoinItem)* + JoinItem <- JoinType :spaces IdentifierItem (:spaces Alias)? (:spaces WithKw :spaces Expression)? + JoinType <- :(InnerKw spaces)? JoinKw (:spaces FetchKw)? + / LeftKw :spaces :(OuterKw spaces)? JoinKw (:spaces FetchKw)? + / RightKw :spaces :(OuterKw spaces)? JoinKw (:spaces FetchKw)? + / FullKw :spaces :(OuterKw spaces)? JoinKw (:spaces FetchKw)? + + # See https://docs.jboss.org/hibernate/orm/3.3/reference/en/html/queryhql.html#queryhql-where + WhereClause <- WhereKw :spaces Expression + + # See https://docs.jboss.org/hibernate/orm/3.3/reference/en/html/queryhql.html#queryhql-ordering + OrderClause <- OrderKw :spaces ByKw :spaces Expression ( :spaces OrderDir )? + OrderDir <- AscKw / DescKw + + # HQL expressions: https://docs.jboss.org/hibernate/orm/3.3/reference/en/html/queryhql.html#queryhql-expressions + # For precedence, see: https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-PRECEDENCE + Expression <- Unary1Expression + / Binary1Expression + / Binary2Expression + / TrinaryExpression + / Unary2Expression + / ParenExpression + / SubQueryExpression + / CallExpression + / IdentifierItem / LitItem / NamedParamItem + # A limited expression missing logical operators, which create ambiguity with 'between a and b'. + LimExpression <- Unary1Expression + / Binary1Expression + / Unary2Expression + / ParenExpression + / SubQueryExpression + / CallExpression + / IdentifierItem / LitItem / NamedParamItem + Binary1Expression <- Expression :spaces Binary1Op :spaces Expression + Binary1Op <- '^' / '*' / '/' / '%' + / '+' / '-' + / InKw / LikeKw / ILikeKw / SimilarKw + / '<' / '>' / '=' / '<=' / '>=' / '<>' + / IsNotKw / IsKw + Binary2Expression <- Expression :spaces Binary2Op :spaces Expression + Binary2Op <- AndKw / OrKw + TrinaryExpression <- Expression :spaces BetweenKw :spaces LimExpression :spaces AndKw :spaces LimExpression + + # A method call, e.g. 'max(age)'. + CallExpression <- Func :spaces? :'(' :spaces? ParameterList :spaces? :')' + Func <- identifier + ParameterList <- Expression ( :spaces? ',' :spaces? Expression )* + ParenExpression <- '(' :spaces? Expression :spaces? ')' + Unary1Expression <- ( '+' / '-' ) !NumberLit Expression + Unary2Expression <- ( NotKw ) :spaces Expression + + SubQueryExpression <- ExistsKw :spaces? :'(' :spaces? SubQuery :spaces? :')' + / Expression :spaces ( InKw / NotInKw ) :spaces :'(' SubQuery :')' + + Identifier <~ ((!Kw identifier) / (Kw identifier)) + IdentifierItem <- Identifier ( :'.' Identifier )* + LitItem <- StringLit / NumberLit / BoolLit / NullLit + NamedParamItem <- ':' identifier + + # See https://learn.microsoft.com/en-us/sql/odbc/reference/appendixes/numeric-literal-syntax?view=sql-server-ver16 + NumberLit <- NumExpLit / SignedNumLit + SignedNumLit <~ [-+]? ;UnsignedNumLit + UnsignedNumLit <~ UnsignedInt ( '.' UnsignedInt? )? + NumExpLit <~ SignedNumLit [Ee] SignedInt + SignedInt <- [-+]? ;UnsignedInt + UnsignedInt <- [0-9]+ + + # Use the syntactic predicate '!' to fail a match if it starts with "'". + StringLit <- "'" ( "''" / ( ! "'" . ) )* "'" + + BoolLit <- TrueKw / FalseKw + NullLit <- NullKw + + Kw <~ AndKw / AsKw / AscKw / AvgKw / BetweenKw / ByKw / CountKw / DeleteKw / DescKw / ExistsKw + / FalseKw / FetchKw / FromKw / FullKw / InnerKw / InKw / ILikeKw / IsKw / JoinKw / LeftKw + / LikeKw / MaxKw / MinKw / NotKw / OrderKw / OrKw / OuterKw / RightKw / SelectKw / SumKw + / TrueKw / WhereKw / WithKw + AndKw <~ [Aa][Nn][Dd] + AsKw <~ [Aa][Ss] + AscKw <~ [Aa][Ss][Cc] + AvgKw <~ [Aa][Vv][Gg] + BetweenKw <~ [Bb][Ee][Tt][Ww][Ee][Ee][Nn] + ByKw <~ [Bb][Yy] + CountKw <~ [Cc][Oo][Uu][Nn][Tt] + DeleteKw <~ [Dd][Ee][Ll][Ee][Tt][Ee] + DescKw <~ [Dd][Ee][Ss][Cc] + ExistsKw <~ [Ee][Xx][Ii][Ss][Tt][Ss] + FalseKw <~ [Ff][Aa][Ll][Ss][Ee] + FetchKw <~ [Ff][Ee][Tt][Cc][Hh] + FromKw <~ [Ff][Rr][Oo][Mm] + FullKw <~ [Ff][Uu][Ll][Ll] + ILikeKw <~ [Ii][Ll][Ii][Kk][Ee] + InnerKw <~ [Ii][Nn][Nn][Ee][Rr] + InKw <~ [Ii][Nn] + IsKw <~ [Ii][Ss] + IsNotKw <~ [Ii][Ss] :spaces [Nn][Oo][Tt] + JoinKw <~ [Jj][Oo][Ii][Nn] + LeftKw <~ [Ll][Ee][Ff][Tt] + LikeKw <~ [Ll][Ii][Kk][Ee] + MaxKw <~ [Mm][Aa][Xx] + MinKw <~ [Mm][Ii][Nn] + NotKw <~ [Nn][Oo][Tt] + NotInKw <~ [Nn][Oo][Tt] :spaces [Ii][Nn] + NullKw <~ [Nn][Uu][Ll][Ll] + OrKw <~ [Oo][Rr] + OrderKw <~ [Oo][Rr][Dd][Ee][Rr] + OuterKw <~ [Oo][Uu][Tt][Ee][Rr] + RightKw <~ [Rr][Ii][Gg][Hh][Tt] + SelectKw <~ [Ss][Ee][Ll][Ee][Cc][Tt] + SimilarKw <~ [Ss][Ii][Mm][Ii][Ll][Aa][Rr] + SumKw <~ [Ss][Uu][Mm] + TrueKw <~ [Tt][Rr][Uu][Ee] + UpdateKw <~ [Uu][Pp][Dd][Aa][Tt][Ee] + WhereKw <~ [Ww][Hh][Ee][Rr][Ee] + WithKw <~ [Ww][Ii][Tt][Hh] + + # End of input, e.g. not any character. + identifierChar <- [a-zA-Z_0-9] + eow <- !identifierChar + eoi <- !. +`)); + +/// A sanity check on the HQL select clause. +unittest { + // Dead simple HQL select. + assert(HQL("FROM models.Fish").successful); + // Select w/ numeric literals. + assert(HQL("SELECT 2, +3, -4, 2., 3.14, -2.45, 3.2e-12 FROM models.Fish").successful); + // Select w/ string literals. + assert(HQL("SELECT 'ham', 'O''Henry' FROM models.Fish").successful); + // Select w/ column names, alias-identifiers, implicit joins. + assert(HQL("SELECT name, f.age, f.species.id FROM models.Fish f").successful); + // Select w/ Parameters + assert(HQL("select :name, :age FROM Ham").successful); + // Select w/ expressions + assert(HQL("select 1 + 2, 1 * (3 + 4), true and false, not true and false FROM Ham").successful); + assert(HQL("select count(h), min(age, height) FROM Ham h").successful); + // Invalid queries. + assert(!HQL("FROM models.Fish floom boom").successful); +} + +/// Test some of HQL's alternate select forms, e.g. as a map or an object. +unittest { + // Delcare a map, which in D could be an associative array or other implementation. + import std.stdio; + assert(HQL("SELECT new map(1 as turn, 2 as magic, age > 18 as is_adult ) FROM Person").successful); + // Declare a new object (assuming a constructor exists). + assert(HQL("SELECT new Birb(1 as turn, 2 as magic, age > 18 as is_adult ) FROM Person").successful); +} + +/// A sanity check on the HQL where clause. +unittest { + // Single values are permitted for where clauses. + assert(HQL("FROM fish WHERE true").successful); + // Binary expressions. + assert(HQL("FROM fish WHERE a < b AND b like '%ham' OR c = 3 and d is NULL or e IS NOT null").successful); + // Trinary expressions. + assert(HQL("from fish where a between 3 and :maxA").successful); + // SubQuery expressions. + assert(HQL("FROM fish where name in (select name from Bird) or " + ~ "exists ( from shark where id = fish.id )").successful); +} + +// A sanity check on HQL order-by clause. +unittest { + // Order by ascending. + assert(HQL("FROM fish ORDER BY age ASC").successful); + // Order by descending. + assert(HQL("FROM fish ORDER BY sister.age desc").successful); + // Implicit sort order (system determined). + assert(HQL("FROM fish ORDER BY sister.age").successful); + // Some invalid ordering that should be rejected. + assert(!HQL("FROM fish ORDER BY sister.age ARSC").successful); + assert(!HQL("FROM fish ORDER BY sister.age ASC DESC").successful); +} + +// A sanity check on HQL joins and fetches. +unittest { + assert(HQL("from Cat as cat " + ~ "inner join cat.mate as mate " + ~ "left outer join cat.kittens as kitten").successful); + assert(HQL("from Cat as cat left join cat.mate.kittens as kittens").successful); + assert(HQL("from Formula form full join form.parameter param").successful); + // join types may be abbreviated + assert(HQL("from Cat as cat " + ~ "join cat.mate as mate " + ~ "left join cat.kittens as kitten").successful); + // extra conditions using the "with" keyword + assert(HQL("from Cat as cat " + ~ "left join cat.kittens as kitten " + ~ "with kitten.bodyWeight > 10.0").successful); + // fetch joins without aliases + assert(HQL("from Cat as cat " + ~ "inner join fetch cat.mate " + ~ "left join fetch cat.kittens").successful); + // fetch joins with aliases + assert(HQL("from Cat as cat " + ~ "inner join fetch cat.mate " + ~ "left join fetch cat.kittens child " + ~ "left join fetch child.kittens").successful); +} + +// A sanity check for HQL update and delete queries. +unittest { + assert(HQL("delete from dogs where name = :Doggo").successful); + assert(HQL("DELETE dogs").successful); +} + +/// A unittest focused on parsing of a select clause in normal form (not list, object, or map). +/// https://docs.jboss.org/hibernate/orm/3.3/reference/en/html/queryhql.html#queryhql-select +unittest { + import std.stdio; + ParseTree pt1 = HQL("SELECT -3.2, fish goober, max(bird) as flappy, -2.3E4 FROM models.Person AS p"); + writeln("pt1 = ", pt1); + assert(pt1.successful); + assert(pt1.children.length == 1 && pt1.children[0].name == "HQL.Query"); + + ParseTree query = pt1.children[0]; + assert(query.children.length == 1 && query.children[0].name == "HQL.SelectQuery"); + + ParseTree selectQuery = query.children[0]; + assert(selectQuery.children.length == 2); + assert(selectQuery.children[0].name == "HQL.SelectClause"); + assert(selectQuery.children[1].name == "HQL.FromClause"); + + ParseTree selectClause = selectQuery.children[0]; + assert(selectClause.children.length == 1 && selectClause.children[0].name == "HQL.ArrayItems"); + + ParseTree arrayItems = selectClause.children[0]; + assert(arrayItems.children.length == 4); + // SelectItem have up to 2 matches: Expression and Alias + assert(arrayItems.children[0].name == "HQL.SelectItem"); + assert(arrayItems.children[0].matches == ["-3.2"]); + assert(arrayItems.children[1].matches == ["fish", "goober"]); + assert(arrayItems.children[2].children[0].children[0].name == "HQL.CallExpression"); + ParseTree callExpression = arrayItems.children[2].children[0].children[0]; + assert(callExpression.children[0].name == "HQL.Func"); + assert(callExpression.children[0].matches == ["max"]); + assert(callExpression.children[1].name == "HQL.ParameterList"); + assert(callExpression.children[1].matches == ["bird"]); + assert(arrayItems.children[3].matches == ["-2.3E4"]); + + ParseTree fromClause = selectQuery.children[1]; + assert(fromClause.children.length == 1); + ParseTree fromItem = fromClause.children[0]; + assert(fromItem.children.length == 2); + assert(fromItem.children[0].name == "HQL.IdentifierItem"); + assert(fromItem.children[0].matches == ["models", "Person"]); + assert(fromItem.children[1].name == "HQL.Alias"); + assert(fromItem.children[1].matches == ["p"]); +} diff --git a/source/hibernated/query.d b/source/hibernated/query.d index 333e89d..ba4d20d 100644 --- a/source/hibernated/query.d +++ b/source/hibernated/query.d @@ -1,1856 +1,1858 @@ -/** - * HibernateD - Object-Relation Mapping for D programming language, with interface similar to Hibernate. - * - * Hibernate documentation can be found here: - * $(LINK http://hibernate.org/docs)$(BR) - * - * Source file hibernated/core.d. - * - * This module contains HQL query parser and HQL to SQL transform implementation. - * - * Copyright: Copyright 2013 - * License: $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0). - * Author: Vadim Lopatin - * - * TODO: Reformat this file to use 4-space indents and LF line endings. - */ -module hibernated.query; - -private import std.ascii; -private import std.algorithm; -private import std.exception; -private import std.array; -private import std.string; -private import std.conv; -//private import std.stdio : writeln; -private import std.variant; - -private import ddbc.core : DataSetWriter; - -import hibernated.annotations; -import hibernated.metadata; -import hibernated.type; -import hibernated.core; -import hibernated.dialect : Dialect; -import hibernated.dialects.mysqldialect; - - -// For backwards compatibily -// 'enforceEx' will be removed with 2.089 -static if(__VERSION__ < 2080) { - alias enforceHelper = enforceEx; -} else { - alias enforceHelper = enforce; -} - -// For backwards compatibily (since D 2.101, logger is no longer in std.experimental) -static if (__traits(compiles, (){ import std.logger; } )) { - import std.logger : trace; -} else { - import std.experimental.logger : trace; -} - -enum JoinType { - InnerJoin, - LeftJoin, -} - -class FromClauseItem { - string entityName; - const EntityInfo entity; - string entityAlias; - string sqlAlias; - int startColumn; - int selectedColumns; - // for JOINs - JoinType joinType = JoinType.InnerJoin; - bool fetch; - FromClauseItem base; - const PropertyInfo baseProperty; - string pathString; - int index; - int selectIndex; - - string getFullPath() { - if (base is null) - return entityAlias; - return base.getFullPath() ~ "." ~ baseProperty.propertyName; - } - - this(const EntityInfo entity, string entityAlias, JoinType joinType, bool fetch, FromClauseItem base = null, const PropertyInfo baseProperty = null) { - this.entityName = entity.name; - this.entity = entity; - this.joinType = joinType; - this.fetch = fetch; - this.base = base; - this.baseProperty = baseProperty; - this.selectIndex = -1; - } - -} - -class FromClause { - FromClauseItem[] items; - FromClauseItem add(const EntityInfo entity, string entityAlias, JoinType joinType, bool fetch, FromClauseItem base = null, const PropertyInfo baseProperty = null) { - FromClauseItem item = new FromClauseItem(entity, entityAlias, joinType, fetch, base, baseProperty); - item.entityAlias = entityAlias is null ? "_a" ~ to!string(items.length + 1) : entityAlias; - item.sqlAlias = "_t" ~ to!string(items.length + 1); - item.index = cast(int)items.length; - item.pathString = item.getFullPath(); - items ~= item; - return item; - } - @property size_t length() { return items.length; } - string getSQL() { - return ""; - } - @property FromClauseItem first() { - return items[0]; - } - FromClauseItem opIndex(int index) { - enforceHelper!HibernatedException(index >= 0 && index < items.length, "FromClause index out of range: " ~ to!string(index)); - return items[index]; - } - FromClauseItem opIndex(string aliasName) { - return findByAlias(aliasName); - } - bool hasAlias(string aliasName) { - foreach(ref m; items) { - if (m.entityAlias == aliasName) - return true; - } - return false; - } - FromClauseItem findByAlias(string aliasName) { - foreach(ref m; items) { - if (m.entityAlias == aliasName) - return m; - } - throw new QuerySyntaxException("Cannot find FROM alias by name " ~ aliasName); - } - FromClauseItem findByPath(string path) { - foreach(ref m; items) { - if (m.pathString == path) - return m; - } - return null; - } -} - -struct OrderByClauseItem { - FromClauseItem from; - PropertyInfo prop; - bool asc; -} - -struct SelectClauseItem { - FromClauseItem from; - PropertyInfo prop; -} - -class QueryParser { - string query; - EntityMetaData metadata; - Token[] tokens; - FromClause fromClause; - //FromClauseItem[] fromClause; - string[] parameterNames; - OrderByClauseItem[] orderByClause; - SelectClauseItem[] selectClause; - Token whereClause; // AST for WHERE expression - - - this(EntityMetaData metadata, string query) { - this.metadata = metadata; - this.query = query; - fromClause = new FromClause(); - //trace("tokenizing query: " ~ query); - tokens = tokenize(query); - //trace("parsing query: " ~ query); - parse(); - //trace("parsing done"); - } - - void parse() { - processParameterNames(0, cast(int)tokens.length); // replace pairs {: Ident} with single Parameter token - int len = cast(int)tokens.length; - //trace("Query tokens: " ~ to!string(len)); - int fromPos = findKeyword(KeywordType.FROM); - int selectPos = findKeyword(KeywordType.SELECT); - int wherePos = findKeyword(KeywordType.WHERE); - int orderPos = findKeyword(KeywordType.ORDER); - enforceHelper!QuerySyntaxException(fromPos >= 0, "No FROM clause in query " ~ query); - enforceHelper!QuerySyntaxException(selectPos <= 0, "SELECT clause should be first - invalid query " ~ query); - enforceHelper!QuerySyntaxException(wherePos == -1 || wherePos > fromPos, "Invalid WHERE position in query " ~ query); - enforceHelper!QuerySyntaxException(orderPos == -1 || (orderPos < tokens.length - 2 && tokens[orderPos + 1].keyword == KeywordType.BY), "Invalid ORDER BY in query " ~ query); - enforceHelper!QuerySyntaxException(orderPos == -1 || orderPos > fromPos, "Invalid ORDER BY position in query " ~ query); - int fromEnd = len; - if (orderPos >= 0) - fromEnd = orderPos; - if (wherePos >= 0) - fromEnd = wherePos; - int whereEnd = wherePos < 0 ? -1 : (orderPos >= 0 ? orderPos : len); - int orderEnd = orderPos < 0 ? -1 : len; - parseFromClause(fromPos + 1, fromEnd); - if (selectPos == 0 && selectPos < fromPos - 1) - parseSelectClause(selectPos + 1, fromPos); - else - defaultSelectClause(); - bool selectedEntities = validateSelectClause(); - if (wherePos >= 0 && whereEnd > wherePos) - parseWhereClause(wherePos + 1, whereEnd); - if (orderPos >= 0 && orderEnd > orderPos) - parseOrderClause(orderPos + 2, orderEnd); - if (selectedEntities) { - processAutoFetchReferences(); - prepareSelectFields(); - } - } - - private void prepareSelectFields() { - int startColumn = 1; - for (int i=0; i < fromClause.length; i++) { - FromClauseItem item = fromClause[i]; - if (!item.fetch) - continue; - int count = item.entity.metadata.getFieldCount(item.entity, false); - if (count > 0) { - item.startColumn = startColumn; - item.selectedColumns = count; - startColumn += count; - } - } - } - - private void processAutoFetchReferences() { - FromClauseItem a = selectClause[0].from; - a.fetch = true; - processAutoFetchReferences(a); - } - - private FromClauseItem ensureItemFetched(FromClauseItem a, const PropertyInfo p) { - FromClauseItem res; - string path = a.pathString ~ "." ~ p.propertyName; - //trace("ensureItemFetched " ~ path); - res = fromClause.findByPath(path); - if (res is null) { - // autoadd join - assert(p.referencedEntity !is null); - res = fromClause.add(p.referencedEntity, null, p.nullable ? JoinType.LeftJoin : JoinType.InnerJoin, true, a, p); - } else { - // force fetch - res.fetch = true; - } - bool selectFound = false; - foreach(s; selectClause) { - if (s.from == res) { - selectFound = true; - break; - } - } - if (!selectFound) { - SelectClauseItem item; - item.from = res; - item.prop = null; - selectClause ~= item; - } - return res; - } - - private bool isBackReferenceProperty(FromClauseItem a, const PropertyInfo p) { - if (a.base is null) - return false; - auto baseEntity = a.base.entity; - assert(baseEntity !is null); - if (p.referencedEntity != baseEntity) - return false; - - if (p.referencedProperty !is null && p.referencedProperty == a.baseProperty) - return true; - if (a.baseProperty.referencedProperty !is null && p == a.baseProperty.referencedProperty) - return true; - return false; - } - - private void processAutoFetchReferences(FromClauseItem a) { - foreach (p; a.entity.properties) { - if (p.lazyLoad) - continue; - if (p.oneToOne && !isBackReferenceProperty(a, p)) { - FromClauseItem res = ensureItemFetched(a, p); - processAutoFetchReferences(res); - } - } - } - - private void updateEntity(const EntityInfo entity, string name) { - foreach(t; tokens) { - if (t.type == TokenType.Ident && t.text == name) { - t.entity = cast(EntityInfo)entity; - t.type = TokenType.Entity; - } - } - } - - private void updateAlias(const EntityInfo entity, string name) { - foreach(t; tokens) { - if (t.type == TokenType.Ident && t.text == name) { - t.entity = cast(EntityInfo)entity; - t.type = TokenType.Alias; - } - } - } - - private void splitCommaDelimitedList(int start, int end, void delegate(int, int) callback) { - //trace("SPLIT " ~ to!string(start) ~ " .. " ~ to!string(end)); - int len = cast(int)tokens.length; - int p = start; - for (int i = start; i < end; i++) { - if (tokens[i].type == TokenType.Comma || i == end - 1) { - enforceHelper!QuerySyntaxException(tokens[i].type != TokenType.Comma || i != end - 1, "Invalid comma at end of list" ~ errorContext(tokens[start])); - int endp = i < end - 1 ? i : end; - enforceHelper!QuerySyntaxException(endp > p, "Invalid comma delimited list" ~ errorContext(tokens[start])); - callback(p, endp); - p = i + 1; - } - } - } - - private int parseFieldRef(int start, int end, ref string[] path) { - int pos = start; - while (pos < end) { - if (tokens[pos].type == TokenType.Ident || tokens[pos].type == TokenType.Alias) { - enforceHelper!QuerySyntaxException(path.length == 0 || tokens[pos].type != TokenType.Alias, "Alias is allowed only as first item" ~ errorContext(tokens[pos])); - path ~= tokens[pos].text; - pos++; - if (pos == end || tokens[pos].type != TokenType.Dot) - return pos; - if (pos == end - 1 || tokens[pos + 1].type != TokenType.Ident) - return pos; - pos++; - } else { - break; - } - } - enforceHelper!QuerySyntaxException(tokens[pos].type != TokenType.Dot, "Unexpected dot at end in field list" ~ errorContext(tokens[pos])); - enforceHelper!QuerySyntaxException(path.length > 0, "Empty field list" ~ errorContext(tokens[pos])); - return pos; - } - - private void parseFirstFromClause(int start, int end, out int pos) { - enforceHelper!QuerySyntaxException(start < end, "Invalid FROM clause " ~ errorContext(tokens[start])); - // minimal support: - // Entity - // Entity alias - // Entity AS alias - enforceHelper!QuerySyntaxException(tokens[start].type == TokenType.Ident, "Entity name identifier expected in FROM clause" ~ errorContext(tokens[start])); - string entityName = cast(string)tokens[start].text; - auto ei = metadata.findEntity(entityName); - updateEntity(ei, entityName); - string aliasName = null; - int p = start + 1; - if (p < end && tokens[p].type == TokenType.Keyword && tokens[p].keyword == KeywordType.AS) - p++; - if (p < end) { - enforceHelper!QuerySyntaxException(tokens[p].type == TokenType.Ident, "Alias name identifier expected in FROM clause" ~ errorContext(tokens[p])); - aliasName = cast(string)tokens[p].text; - p++; - } - if (aliasName != null) - updateAlias(ei, aliasName); - fromClause.add(ei, aliasName, JoinType.InnerJoin, false); - pos = p; - } - - void appendFromClause(Token context, string[] path, string aliasName, JoinType joinType, bool fetch) { - int p = 0; - enforceHelper!QuerySyntaxException(fromClause.hasAlias(path[p]), "Unknown alias " ~ path[p] ~ " in FROM clause" ~ errorContext(context)); - FromClauseItem baseClause = findFromClauseByAlias(path[p]); - //string pathString = path[p]; - p++; - while(true) { - auto baseEntity = baseClause.entity; - enforceHelper!QuerySyntaxException(p < path.length, "Property name expected in FROM clause" ~ errorContext(context)); - string propertyName = path[p++]; - auto property = baseEntity[propertyName]; - auto referencedEntity = property.referencedEntity; - assert(referencedEntity !is null); - enforceHelper!QuerySyntaxException(!property.simple, "Simple property " ~ propertyName ~ " cannot be used in JOIN" ~ errorContext(context)); - enforceHelper!QuerySyntaxException(!property.embedded, "Embedded property " ~ propertyName ~ " cannot be used in JOIN" ~ errorContext(context)); - bool last = (p == path.length); - FromClauseItem item = fromClause.add(referencedEntity, last ? aliasName : null, joinType, fetch, baseClause, property); - if (last && aliasName !is null) - updateAlias(referencedEntity, item.entityAlias); - baseClause = item; - if (last) - break; - } - } - - void parseFromClause(int start, int end) { - int p = start; - parseFirstFromClause(start, end, p); - while (p < end) { - Token context = tokens[p]; - JoinType joinType = JoinType.InnerJoin; - if (tokens[p].keyword == KeywordType.LEFT) { - joinType = JoinType.LeftJoin; - p++; - } else if (tokens[p].keyword == KeywordType.INNER) { - p++; - } - enforceHelper!QuerySyntaxException(p < end && tokens[p].keyword == KeywordType.JOIN, "Invalid FROM clause" ~ errorContext(tokens[p])); - p++; - enforceHelper!QuerySyntaxException(p < end, "Invalid FROM clause - incomplete JOIN" ~ errorContext(tokens[p])); - bool fetch = false; - if (tokens[p].keyword == KeywordType.FETCH) { - fetch = true; - p++; - enforceHelper!QuerySyntaxException(p < end, "Invalid FROM clause - incomplete JOIN" ~ errorContext(tokens[p])); - } - string[] path; - p = parseFieldRef(p, end, path); - string aliasName; - bool hasAS = false; - if (p < end && tokens[p].keyword == KeywordType.AS) { - p++; - hasAS = true; - } - enforceHelper!QuerySyntaxException(p < end && tokens[p].type == TokenType.Ident, "Invalid FROM clause - no alias in JOIN" ~ errorContext(tokens[p])); - aliasName = tokens[p].text; - p++; - appendFromClause(context, path, aliasName, joinType, fetch); - } - enforceHelper!QuerySyntaxException(p == end, "Invalid FROM clause" ~ errorContext(tokens[p])); - } - - // in pairs {: Ident} replace type of ident with Parameter - void processParameterNames(int start, int end) { - for (int i = start; i < end; i++) { - if (tokens[i].type == TokenType.Parameter) { - parameterNames ~= cast(string)tokens[i].text; - } - } - } - - FromClauseItem findFromClauseByAlias(string aliasName) { - return fromClause.findByAlias(aliasName); - } - - void addSelectClauseItem(string aliasName, string[] propertyNames) { - //trace("addSelectClauseItem alias=" ~ aliasName ~ " properties=" ~ to!string(propertyNames)); - FromClauseItem from = aliasName == null ? fromClause.first : findFromClauseByAlias(aliasName); - SelectClauseItem item; - item.from = from; - item.prop = null; - EntityInfo ei = cast(EntityInfo)from.entity; - if (propertyNames.length > 0) { - item.prop = cast(PropertyInfo)ei.findProperty(propertyNames[0]); - propertyNames.popFront(); - while (item.prop.embedded) { - //trace("Embedded property " ~ item.prop.propertyName ~ " of type " ~ item.prop.referencedEntityName); - ei = cast(EntityInfo)item.prop.referencedEntity; - enforceHelper!QuerySyntaxException(propertyNames.length > 0, "@Embedded field property name should be specified when selecting " ~ aliasName ~ "." ~ item.prop.propertyName); - item.prop = cast(PropertyInfo)ei.findProperty(propertyNames[0]); - propertyNames.popFront(); - } - } - enforceHelper!QuerySyntaxException(propertyNames.length == 0, "Extra field names in SELECT clause in query " ~ query); - selectClause ~= item; - //insertInPlace(selectClause, 0, item); - } - - //void addOrderByClauseItem(string aliasName, string propertyName, bool asc) { - void addOrderByClauseItem(FromClauseItem from, PropertyInfo prop, bool asc) { - OrderByClauseItem item; - item.from = from; - item.prop = prop; - item.asc = asc; - orderByClause ~= item; - //insertInPlace(orderByClause, 0, item); - } - - void parseOrderByClauseItem(int start, int end) { - Token orderClauseItem = new Token(tokens[start].pos, TokenType.Expression, tokens, start, end); - // Convert any `[.][.]...` token sequences into fields. - // E.g. consider the ORDER BY fields in HQL `FROM Cats c ORDER BY c.age DESC, c.license.id ASC`. - convertFields(orderClauseItem.children); - - enforceHelper!QuerySyntaxException( - orderClauseItem.children.length >= 1 && orderClauseItem.children.length <= 2 - && orderClauseItem.children[0].type == TokenType.Field - && (orderClauseItem.children.length == 1 || orderClauseItem.children[1].type == TokenType.Keyword), - "Invalid ORDER BY clause (expected {property [ASC | DESC]} or {alias.property [ASC | DESC]} )" ~ errorContext(tokens[start])); - - trace("ORDER BY ITEM: " ~ to!string(start) ~ " .. " ~ to!string(end)); - bool asc = true; - if (orderClauseItem.children[$-1].type == TokenType.Keyword && orderClauseItem.children[$-1].keyword == KeywordType.DESC) { - asc = false; - } - addOrderByClauseItem(orderClauseItem.children[0].from, orderClauseItem.children[0].field, asc); - } - - void parseSelectClauseItem(int start, int end) { - // for each comma delimited item - // in current version it can only be - // {property} or {alias . property} - //trace("SELECT ITEM: " ~ to!string(start) ~ " .. " ~ to!string(end)); - 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])); - string aliasName; - int p = start; - if (tokens[p].type == TokenType.Alias) { - //trace("select clause alias: " ~ tokens[p].text ~ " query: " ~ query); - aliasName = cast(string)tokens[p].text; - p++; - enforceHelper!QuerySyntaxException(p == end || tokens[p].type == TokenType.Dot, "SELECT clause item is invalid (only [alias.]field{[.field2]}+ allowed) " ~ errorContext(tokens[start])); - if (p < end - 1 && tokens[p].type == TokenType.Dot) - p++; - } else { - //trace("select clause non-alias: " ~ tokens[p].text ~ " query: " ~ query); - } - string[] fieldNames; - while (p < end && tokens[p].type == TokenType.Ident) { - fieldNames ~= tokens[p].text; - p++; - if (p > end - 1 || tokens[p].type != TokenType.Dot) - break; - // skipping dot - p++; - } - //trace("parseSelectClauseItem pos=" ~ to!string(p) ~ " end=" ~ to!string(end)); - enforceHelper!QuerySyntaxException(p >= end, "SELECT clause item is invalid (only [alias.]field{[.field2]}+ allowed) " ~ errorContext(tokens[start])); - addSelectClauseItem(aliasName, fieldNames); - } - - void parseSelectClause(int start, int end) { - enforceHelper!QuerySyntaxException(start < end, "Invalid SELECT clause" ~ errorContext(tokens[start])); - splitCommaDelimitedList(start, end, &parseSelectClauseItem); - } - - void defaultSelectClause() { - addSelectClauseItem(fromClause.first.entityAlias, null); - } - - bool validateSelectClause() { - enforceHelper!QuerySyntaxException(selectClause != null && selectClause.length > 0, "Invalid SELECT clause"); - int aliasCount = 0; - int fieldCount = 0; - foreach(a; selectClause) { - if (a.prop !is null) - fieldCount++; - else - aliasCount++; - } - 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"); - return aliasCount > 0; - } - - void parseWhereClause(int start, int end) { - enforceHelper!QuerySyntaxException(start < end, "Invalid WHERE clause" ~ errorContext(tokens[start])); - whereClause = new Token(tokens[start].pos, TokenType.Expression, tokens, start, end); - //trace("before convert fields:\n" ~ whereClause.dump(0)); - convertFields(whereClause.children); - //trace("after convertFields before convertIsNullIsNotNull:\n" ~ whereClause.dump(0)); - convertIsNullIsNotNull(whereClause.children); - //trace("after convertIsNullIsNotNull\n" ~ whereClause.dump(0)); - convertUnaryPlusMinus(whereClause.children); - //trace("after convertUnaryPlusMinus\n" ~ whereClause.dump(0)); - foldBraces(whereClause.children); - //trace("after foldBraces\n" ~ whereClause.dump(0)); - foldOperators(whereClause.children); - //trace("after foldOperators\n" ~ whereClause.dump(0)); - dropBraces(whereClause.children); - //trace("after dropBraces\n" ~ whereClause.dump(0)); - } - - void foldBraces(ref Token[] items) { - while (true) { - if (items.length == 0) - return; - int lastOpen = -1; - int firstClose = -1; - for (int i=0; i= 0 && lastOpen < firstClose, "Unpaired braces in WHERE clause" ~ errorContext(tokens[lastOpen])); - Token folded = new Token(items[lastOpen].pos, TokenType.Braces, items, lastOpen + 1, firstClose); - // size_t oldlen = items.length; - // int removed = firstClose - lastOpen; - replaceInPlace(items, lastOpen, firstClose + 1, [folded]); - // assert(items.length == oldlen - removed); - foldBraces(folded.children); - } - } - - static void dropBraces(ref Token[] items) { - foreach (t; items) { - if (t.children.length > 0) - dropBraces(t.children); - } - for (int i=0; i= 0; i--) { - if (items[i].type != TokenType.Operator || items[i + 1].type != TokenType.Keyword) - continue; - if (items[i].operator == OperatorType.IS && items[i + 1].keyword == KeywordType.NULL) { - Token folded = new Token(items[i].pos,OperatorType.IS_NULL, "IS NULL"); - replaceInPlace(items, i, i + 2, [folded]); - i-=2; - } - } - for (int i = cast(int)items.length - 3; i >= 0; i--) { - if (items[i].type != TokenType.Operator || items[i + 1].type != TokenType.Operator || items[i + 2].type != TokenType.Keyword) - continue; - if (items[i].operator == OperatorType.IS && items[i + 1].operator == OperatorType.NOT && items[i + 2].keyword == KeywordType.NULL) { - Token folded = new Token(items[i].pos, OperatorType.IS_NOT_NULL, "IS NOT NULL"); - replaceInPlace(items, i, i + 3, [folded]); - i-=3; - } - } - } - - /** - * During the parsing of an HQL query, populates Tokens with TokenType Ident or Alias with - * contextual information such as EntityInfo, PropertyInfo, and more. - */ - void convertFields(ref Token[] items) { - while(true) { - // Find the first Ident/Alias token and keep its index in `p`. - int p = -1; - for (int i=0; i.] [.]... - // Extract only the Alias and Ident tokens, without Dot tokens, and store that in `idents`. - // Each dot represents either an @Embeddable property or a join. - string[] idents; - int lastp = p; - idents ~= items[p].text; - for (int i=p + 1; i < items.length - 1; i+=2) { - if (items[i].type != TokenType.Dot) - break; - enforceHelper!QuerySyntaxException( - i < items.length - 1 && items[i + 1].type == TokenType.Ident, - "Syntax error in property - no property name after . " ~ errorContext(items[p])); - lastp = i + 1; - idents ~= items[i + 1].text; - } - - // Determine the entity in the FromClause being referred to and make `idents` contain only Ident tokens. - FromClauseItem a; - if (items[p].type == TokenType.Alias) { - a = findFromClauseByAlias(idents[0]); - idents.popFront(); - } else { - // use first FROM clause if alias is not specified - a = fromClause.first; - } - string aliasName = a.entityAlias; - EntityInfo ei = cast(EntityInfo)a.entity; - enforceHelper!QuerySyntaxException(idents.length > 0, - "Syntax error in property - alias w/o property name: " ~ aliasName ~ errorContext(items[p])); - - // Dive through the list of identifiers, searching through embedded properties and joins. - PropertyInfo pi; - string fullName; - string columnPrefix; - fullName = aliasName; - while(true) { - string propertyName = idents[0]; - idents.popFront(); - fullName ~= "." ~ propertyName; - pi = cast(PropertyInfo)ei.findProperty(propertyName); - - // First consume all the dot-separated identifiers for @Embedded properties. - while (pi.embedded) { // loop to allow nested @Embedded - enforceHelper!QuerySyntaxException( - idents.length > 0, - "Syntax error in property - @Embedded property reference should include reference to @Embeddable property " - ~ aliasName ~ errorContext(items[p])); - // The `columnName` of an `@Embedded` property contains an optional prefix to add to the - // column names of the properties inside its entity type. - columnPrefix ~= pi.columnName == "" ? pi.columnName : pi.columnName ~ "_"; - propertyName = idents[0]; - idents.popFront(); - pi = cast(PropertyInfo)pi.referencedEntity.findProperty(propertyName); - fullName = fullName ~ "." ~ propertyName; - } - if (idents.length == 0) - break; - - // Next consume all the dot-separated identifiers for explicit or implicit joins. - if (idents.length > 0) { - // There are more names after `propertyName`, whose property info is in `pi`. - string pname = idents[0]; - enforceHelper!QuerySyntaxException(pi.referencedEntity !is null, "Unexpected extra field name " ~ pname ~ " - property " ~ propertyName ~ " doesn't content subproperties " ~ errorContext(items[p])); - ei = cast(EntityInfo)pi.referencedEntity; +/** + * HibernateD - Object-Relation Mapping for D programming language, with interface similar to Hibernate. + * + * Hibernate documentation can be found here: + * $(LINK http://hibernate.org/docs)$(BR) + * + * Source file hibernated/core.d. + * + * This module contains HQL query parser and HQL to SQL transform implementation. + * + * Copyright: Copyright 2013 + * License: $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0). + * Author: Vadim Lopatin + * + * TODO: Reformat this file to use 4-space indents and LF line endings. + */ +module hibernated.query; + +private import std.ascii; +private import std.algorithm; +private import std.exception; +private import std.array; +private import std.string; +private import std.conv; +//private import std.stdio : writeln; +private import std.variant; + +private import ddbc.core : DataSetWriter; + +import hibernated.annotations; +import hibernated.metadata; +import hibernated.type; +import hibernated.core; +import hibernated.dialect : Dialect; +import hibernated.dialects.mysqldialect; + + +// For backwards compatibily +// 'enforceEx' will be removed with 2.089 +static if(__VERSION__ < 2080) { + alias enforceHelper = enforceEx; +} else { + alias enforceHelper = enforce; +} + +// For backwards compatibily (since D 2.101, logger is no longer in std.experimental) +static if (__traits(compiles, (){ import std.logger; } )) { + import std.logger : trace; +} else { + import std.experimental.logger : trace; +} + +enum JoinType { + InnerJoin, + LeftJoin, +} + +class FromClauseItem { + string entityName; + const EntityInfo entity; + string entityAlias; + string sqlAlias; + int startColumn; + int selectedColumns; + // for JOINs + JoinType joinType = JoinType.InnerJoin; + bool fetch; + FromClauseItem base; + const PropertyInfo baseProperty; + string pathString; + int index; + int selectIndex; + + string getFullPath() { + if (base is null) + return entityAlias; + return base.getFullPath() ~ "." ~ baseProperty.propertyName; + } + + this(const EntityInfo entity, string entityAlias, JoinType joinType, bool fetch, FromClauseItem base = null, const PropertyInfo baseProperty = null) { + this.entityName = entity.name; + this.entity = entity; + this.joinType = joinType; + this.fetch = fetch; + this.base = base; + this.baseProperty = baseProperty; + this.selectIndex = -1; + } + +} + +class FromClause { + FromClauseItem[] items; + FromClauseItem add(const EntityInfo entity, string entityAlias, JoinType joinType, bool fetch, FromClauseItem base = null, const PropertyInfo baseProperty = null) { + FromClauseItem item = new FromClauseItem(entity, entityAlias, joinType, fetch, base, baseProperty); + item.entityAlias = entityAlias is null ? "_a" ~ to!string(items.length + 1) : entityAlias; + item.sqlAlias = "_t" ~ to!string(items.length + 1); + item.index = cast(int)items.length; + item.pathString = item.getFullPath(); + items ~= item; + return item; + } + @property size_t length() { return items.length; } + string getSQL() { + return ""; + } + @property FromClauseItem first() { + return items[0]; + } + FromClauseItem opIndex(int index) { + enforceHelper!HibernatedException(index >= 0 && index < items.length, "FromClause index out of range: " ~ to!string(index)); + return items[index]; + } + FromClauseItem opIndex(string aliasName) { + return findByAlias(aliasName); + } + bool hasAlias(string aliasName) { + foreach(ref m; items) { + if (m.entityAlias == aliasName) + return true; + } + return false; + } + FromClauseItem findByAlias(string aliasName) { + foreach(ref m; items) { + if (m.entityAlias == aliasName) + return m; + } + throw new QuerySyntaxException("Cannot find FROM alias by name " ~ aliasName); + } + FromClauseItem findByPath(string path) { + foreach(ref m; items) { + if (m.pathString == path) + return m; + } + return null; + } +} + +struct OrderByClauseItem { + FromClauseItem from; + PropertyInfo prop; + bool asc; +} + +struct SelectClauseItem { + FromClauseItem from; + PropertyInfo prop; +} + +class QueryParser { + string query; + EntityMetaData metadata; + Token[] tokens; + FromClause fromClause; + //FromClauseItem[] fromClause; + string[] parameterNames; + OrderByClauseItem[] orderByClause; + SelectClauseItem[] selectClause; + Token whereClause; // AST for WHERE expression + + + this(EntityMetaData metadata, string query) { + this.metadata = metadata; + this.query = query; + fromClause = new FromClause(); + //trace("tokenizing query: " ~ query); + tokens = tokenize(query); + //trace("parsing query: " ~ query); + parse(); + //trace("parsing done"); + } + + void parse() { + processParameterNames(0, cast(int)tokens.length); // replace pairs {: Ident} with single Parameter token + int len = cast(int)tokens.length; + //trace("Query tokens: " ~ to!string(len)); + int fromPos = findKeyword(KeywordType.FROM); + int selectPos = findKeyword(KeywordType.SELECT); + int wherePos = findKeyword(KeywordType.WHERE); + int orderPos = findKeyword(KeywordType.ORDER); + enforceHelper!QuerySyntaxException(fromPos >= 0, "No FROM clause in query " ~ query); + enforceHelper!QuerySyntaxException(selectPos <= 0, "SELECT clause should be first - invalid query " ~ query); + enforceHelper!QuerySyntaxException(wherePos == -1 || wherePos > fromPos, "Invalid WHERE position in query " ~ query); + enforceHelper!QuerySyntaxException(orderPos == -1 || (orderPos < tokens.length - 2 && tokens[orderPos + 1].keyword == KeywordType.BY), "Invalid ORDER BY in query " ~ query); + enforceHelper!QuerySyntaxException(orderPos == -1 || orderPos > fromPos, "Invalid ORDER BY position in query " ~ query); + int fromEnd = len; + if (orderPos >= 0) + fromEnd = orderPos; + if (wherePos >= 0) + fromEnd = wherePos; + int whereEnd = wherePos < 0 ? -1 : (orderPos >= 0 ? orderPos : len); + int orderEnd = orderPos < 0 ? -1 : len; + parseFromClause(fromPos + 1, fromEnd); + if (selectPos == 0 && selectPos < fromPos - 1) + parseSelectClause(selectPos + 1, fromPos); + else + defaultSelectClause(); + bool selectedEntities = validateSelectClause(); + if (wherePos >= 0 && whereEnd > wherePos) + parseWhereClause(wherePos + 1, whereEnd); + if (orderPos >= 0 && orderEnd > orderPos) + parseOrderClause(orderPos + 2, orderEnd); + if (selectedEntities) { + processAutoFetchReferences(); + prepareSelectFields(); + } + } + + private void prepareSelectFields() { + int startColumn = 1; + for (int i=0; i < fromClause.length; i++) { + FromClauseItem item = fromClause[i]; + if (!item.fetch) + continue; + int count = item.entity.metadata.getFieldCount(item.entity, false); + if (count > 0) { + item.startColumn = startColumn; + item.selectedColumns = count; + startColumn += count; + } + } + } + + private void processAutoFetchReferences() { + FromClauseItem a = selectClause[0].from; + a.fetch = true; + processAutoFetchReferences(a); + } + + private FromClauseItem ensureItemFetched(FromClauseItem a, const PropertyInfo p) { + FromClauseItem res; + string path = a.pathString ~ "." ~ p.propertyName; + //trace("ensureItemFetched " ~ path); + res = fromClause.findByPath(path); + if (res is null) { + // autoadd join + assert(p.referencedEntity !is null); + res = fromClause.add(p.referencedEntity, null, p.nullable ? JoinType.LeftJoin : JoinType.InnerJoin, true, a, p); + } else { + // force fetch + res.fetch = true; + } + bool selectFound = false; + foreach(s; selectClause) { + if (s.from == res) { + selectFound = true; + break; + } + } + if (!selectFound) { + SelectClauseItem item; + item.from = res; + item.prop = null; + selectClause ~= item; + } + return res; + } + + private bool isBackReferenceProperty(FromClauseItem a, const PropertyInfo p) { + if (a.base is null) + return false; + auto baseEntity = a.base.entity; + assert(baseEntity !is null); + if (p.referencedEntity != baseEntity) + return false; + + if (p.referencedProperty !is null && p.referencedProperty == a.baseProperty) + return true; + if (a.baseProperty.referencedProperty !is null && p == a.baseProperty.referencedProperty) + return true; + return false; + } + + private void processAutoFetchReferences(FromClauseItem a) { + foreach (p; a.entity.properties) { + if (p.lazyLoad) + continue; + if (p.oneToOne && !isBackReferenceProperty(a, p)) { + FromClauseItem res = ensureItemFetched(a, p); + processAutoFetchReferences(res); + } + } + } + + private void updateEntity(const EntityInfo entity, string name) { + foreach(t; tokens) { + if (t.type == TokenType.Ident && t.text == name) { + t.entity = cast(EntityInfo)entity; + t.type = TokenType.Entity; + } + } + } + + private void updateAlias(const EntityInfo entity, string name) { + foreach(t; tokens) { + if (t.type == TokenType.Ident && t.text == name) { + t.entity = cast(EntityInfo)entity; + t.type = TokenType.Alias; + } + } + } + + private void splitCommaDelimitedList(int start, int end, void delegate(int, int) callback) { + //trace("SPLIT " ~ to!string(start) ~ " .. " ~ to!string(end)); + int len = cast(int)tokens.length; + int p = start; + for (int i = start; i < end; i++) { + if (tokens[i].type == TokenType.Comma || i == end - 1) { + enforceHelper!QuerySyntaxException(tokens[i].type != TokenType.Comma || i != end - 1, "Invalid comma at end of list" ~ errorContext(tokens[start])); + int endp = i < end - 1 ? i : end; + enforceHelper!QuerySyntaxException(endp > p, "Invalid comma delimited list" ~ errorContext(tokens[start])); + callback(p, endp); + p = i + 1; + } + } + } + + private int parseFieldRef(int start, int end, ref string[] path) { + int pos = start; + while (pos < end) { + if (tokens[pos].type == TokenType.Ident || tokens[pos].type == TokenType.Alias) { + enforceHelper!QuerySyntaxException(path.length == 0 || tokens[pos].type != TokenType.Alias, "Alias is allowed only as first item" ~ errorContext(tokens[pos])); + path ~= tokens[pos].text; + pos++; + if (pos == end || tokens[pos].type != TokenType.Dot) + return pos; + if (pos == end - 1 || tokens[pos + 1].type != TokenType.Ident) + return pos; + pos++; + } else { + break; + } + } + enforceHelper!QuerySyntaxException(tokens[pos].type != TokenType.Dot, "Unexpected dot at end in field list" ~ errorContext(tokens[pos])); + enforceHelper!QuerySyntaxException(path.length > 0, "Empty field list" ~ errorContext(tokens[pos])); + return pos; + } + + private void parseFirstFromClause(int start, int end, out int pos) { + enforceHelper!QuerySyntaxException(start < end, "Invalid FROM clause " ~ errorContext(tokens[start])); + // minimal support: + // Entity + // Entity alias + // Entity AS alias + enforceHelper!QuerySyntaxException(tokens[start].type == TokenType.Ident, "Entity name identifier expected in FROM clause" ~ errorContext(tokens[start])); + string entityName = cast(string)tokens[start].text; + auto ei = metadata.findEntity(entityName); + updateEntity(ei, entityName); + string aliasName = null; + int p = start + 1; + if (p < end && tokens[p].type == TokenType.Keyword && tokens[p].keyword == KeywordType.AS) + p++; + if (p < end) { + enforceHelper!QuerySyntaxException(tokens[p].type == TokenType.Ident, "Alias name identifier expected in FROM clause" ~ errorContext(tokens[p])); + aliasName = cast(string)tokens[p].text; + p++; + } + if (aliasName != null) + updateAlias(ei, aliasName); + fromClause.add(ei, aliasName, JoinType.InnerJoin, false); + pos = p; + } + + void appendFromClause(Token context, string[] path, string aliasName, JoinType joinType, bool fetch) { + int p = 0; + enforceHelper!QuerySyntaxException(fromClause.hasAlias(path[p]), "Unknown alias " ~ path[p] ~ " in FROM clause" ~ errorContext(context)); + FromClauseItem baseClause = findFromClauseByAlias(path[p]); + //string pathString = path[p]; + p++; + while(true) { + auto baseEntity = baseClause.entity; + enforceHelper!QuerySyntaxException(p < path.length, "Property name expected in FROM clause" ~ errorContext(context)); + string propertyName = path[p++]; + auto property = baseEntity[propertyName]; + auto referencedEntity = property.referencedEntity; + assert(referencedEntity !is null); + enforceHelper!QuerySyntaxException(!property.simple, "Simple property " ~ propertyName ~ " cannot be used in JOIN" ~ errorContext(context)); + enforceHelper!QuerySyntaxException(!property.embedded, "Embedded property " ~ propertyName ~ " cannot be used in JOIN" ~ errorContext(context)); + bool last = (p == path.length); + FromClauseItem item = fromClause.add(referencedEntity, last ? aliasName : null, joinType, fetch, baseClause, property); + if (last && aliasName !is null) + updateAlias(referencedEntity, item.entityAlias); + baseClause = item; + if (last) + break; + } + } + + void parseFromClause(int start, int end) { + int p = start; + parseFirstFromClause(start, end, p); + while (p < end) { + Token context = tokens[p]; + JoinType joinType = JoinType.InnerJoin; + if (tokens[p].keyword == KeywordType.LEFT) { + joinType = JoinType.LeftJoin; + p++; + } else if (tokens[p].keyword == KeywordType.INNER) { + p++; + } + enforceHelper!QuerySyntaxException(p < end && tokens[p].keyword == KeywordType.JOIN, "Invalid FROM clause" ~ errorContext(tokens[p])); + p++; + enforceHelper!QuerySyntaxException(p < end, "Invalid FROM clause - incomplete JOIN" ~ errorContext(tokens[p])); + bool fetch = false; + if (tokens[p].keyword == KeywordType.FETCH) { + fetch = true; + p++; + enforceHelper!QuerySyntaxException(p < end, "Invalid FROM clause - incomplete JOIN" ~ errorContext(tokens[p])); + } + string[] path; + p = parseFieldRef(p, end, path); + string aliasName; + bool hasAS = false; + if (p < end && tokens[p].keyword == KeywordType.AS) { + p++; + hasAS = true; + } + enforceHelper!QuerySyntaxException(p < end && tokens[p].type == TokenType.Ident, "Invalid FROM clause - no alias in JOIN" ~ errorContext(tokens[p])); + aliasName = tokens[p].text; + p++; + appendFromClause(context, path, aliasName, joinType, fetch); + } + enforceHelper!QuerySyntaxException(p == end, "Invalid FROM clause" ~ errorContext(tokens[p])); + } + + // in pairs {: Ident} replace type of ident with Parameter + void processParameterNames(int start, int end) { + for (int i = start; i < end; i++) { + if (tokens[i].type == TokenType.Parameter) { + parameterNames ~= cast(string)tokens[i].text; + } + } + } + + FromClauseItem findFromClauseByAlias(string aliasName) { + return fromClause.findByAlias(aliasName); + } + + void addSelectClauseItem(string aliasName, string[] propertyNames) { + //trace("addSelectClauseItem alias=" ~ aliasName ~ " properties=" ~ to!string(propertyNames)); + FromClauseItem from = aliasName == null ? fromClause.first : findFromClauseByAlias(aliasName); + SelectClauseItem item; + item.from = from; + item.prop = null; + EntityInfo ei = cast(EntityInfo)from.entity; + if (propertyNames.length > 0) { + item.prop = cast(PropertyInfo)ei.findProperty(propertyNames[0]); + propertyNames.popFront(); + while (item.prop.embedded) { + //trace("Embedded property " ~ item.prop.propertyName ~ " of type " ~ item.prop.referencedEntityName); + ei = cast(EntityInfo)item.prop.referencedEntity; + enforceHelper!QuerySyntaxException(propertyNames.length > 0, "@Embedded field property name should be specified when selecting " ~ aliasName ~ "." ~ item.prop.propertyName); + item.prop = cast(PropertyInfo)ei.findProperty(propertyNames[0]); + propertyNames.popFront(); + } + } + enforceHelper!QuerySyntaxException(propertyNames.length == 0, "Extra field names in SELECT clause in query " ~ query); + selectClause ~= item; + //insertInPlace(selectClause, 0, item); + } + + //void addOrderByClauseItem(string aliasName, string propertyName, bool asc) { + void addOrderByClauseItem(FromClauseItem from, PropertyInfo prop, bool asc) { + OrderByClauseItem item; + item.from = from; + item.prop = prop; + item.asc = asc; + orderByClause ~= item; + //insertInPlace(orderByClause, 0, item); + } + + void parseOrderByClauseItem(int start, int end) { + Token orderClauseItem = new Token(tokens[start].pos, TokenType.Expression, tokens, start, end); + // Convert any `[.][.]...` token sequences into fields. + // E.g. consider the ORDER BY fields in HQL `FROM Cats c ORDER BY c.age DESC, c.license.id ASC`. + convertFields(orderClauseItem.children); + + enforceHelper!QuerySyntaxException( + orderClauseItem.children.length >= 1 && orderClauseItem.children.length <= 2 + && orderClauseItem.children[0].type == TokenType.Field + && (orderClauseItem.children.length == 1 || orderClauseItem.children[1].type == TokenType.Keyword), + "Invalid ORDER BY clause (expected {property [ASC | DESC]} or {alias.property [ASC | DESC]} )" ~ errorContext(tokens[start])); + + trace("ORDER BY ITEM: " ~ to!string(start) ~ " .. " ~ to!string(end)); + bool asc = true; + if (orderClauseItem.children[$-1].type == TokenType.Keyword && orderClauseItem.children[$-1].keyword == KeywordType.DESC) { + asc = false; + } + addOrderByClauseItem(orderClauseItem.children[0].from, orderClauseItem.children[0].field, asc); + } + + void parseSelectClauseItem(int start, int end) { + // for each comma delimited item + // in current version it can only be + // {property} or {alias . property} + //trace("SELECT ITEM: " ~ to!string(start) ~ " .. " ~ to!string(end)); + 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])); + string aliasName; + int p = start; + if (tokens[p].type == TokenType.Alias) { + //trace("select clause alias: " ~ tokens[p].text ~ " query: " ~ query); + aliasName = cast(string)tokens[p].text; + p++; + enforceHelper!QuerySyntaxException(p == end || tokens[p].type == TokenType.Dot, "SELECT clause item is invalid (only [alias.]field{[.field2]}+ allowed) " ~ errorContext(tokens[start])); + if (p < end - 1 && tokens[p].type == TokenType.Dot) + p++; + } else { + //trace("select clause non-alias: " ~ tokens[p].text ~ " query: " ~ query); + } + string[] fieldNames; + while (p < end && tokens[p].type == TokenType.Ident) { + fieldNames ~= tokens[p].text; + p++; + if (p > end - 1 || tokens[p].type != TokenType.Dot) + break; + // skipping dot + p++; + } + //trace("parseSelectClauseItem pos=" ~ to!string(p) ~ " end=" ~ to!string(end)); + enforceHelper!QuerySyntaxException(p >= end, "SELECT clause item is invalid (only [alias.]field{[.field2]}+ allowed) " ~ errorContext(tokens[start])); + addSelectClauseItem(aliasName, fieldNames); + } + + void parseSelectClause(int start, int end) { + enforceHelper!QuerySyntaxException(start < end, "Invalid SELECT clause" ~ errorContext(tokens[start])); + splitCommaDelimitedList(start, end, &parseSelectClauseItem); + } + + void defaultSelectClause() { + addSelectClauseItem(fromClause.first.entityAlias, null); + } + + bool validateSelectClause() { + enforceHelper!QuerySyntaxException(selectClause != null && selectClause.length > 0, "Invalid SELECT clause"); + int aliasCount = 0; + int fieldCount = 0; + foreach(a; selectClause) { + if (a.prop !is null) + fieldCount++; + else + aliasCount++; + } + 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"); + return aliasCount > 0; + } + + void parseWhereClause(int start, int end) { + enforceHelper!QuerySyntaxException(start < end, "Invalid WHERE clause" ~ errorContext(tokens[start])); + whereClause = new Token(tokens[start].pos, TokenType.Expression, tokens, start, end); + //trace("before convert fields:\n" ~ whereClause.dump(0)); + convertFields(whereClause.children); + //trace("after convertFields before convertIsNullIsNotNull:\n" ~ whereClause.dump(0)); + convertIsNullIsNotNull(whereClause.children); + //trace("after convertIsNullIsNotNull\n" ~ whereClause.dump(0)); + convertUnaryPlusMinus(whereClause.children); + //trace("after convertUnaryPlusMinus\n" ~ whereClause.dump(0)); + foldBraces(whereClause.children); + //trace("after foldBraces\n" ~ whereClause.dump(0)); + foldOperators(whereClause.children); + //trace("after foldOperators\n" ~ whereClause.dump(0)); + dropBraces(whereClause.children); + //trace("after dropBraces\n" ~ whereClause.dump(0)); + } + + void foldBraces(ref Token[] items) { + while (true) { + if (items.length == 0) + return; + int lastOpen = -1; + int firstClose = -1; + for (int i=0; i= 0 && lastOpen < firstClose, "Unpaired braces in WHERE clause" ~ errorContext(tokens[lastOpen])); + Token folded = new Token(items[lastOpen].pos, TokenType.Braces, items, lastOpen + 1, firstClose); + // size_t oldlen = items.length; + // int removed = firstClose - lastOpen; + replaceInPlace(items, lastOpen, firstClose + 1, [folded]); + // assert(items.length == oldlen - removed); + foldBraces(folded.children); + } + } + + static void dropBraces(ref Token[] items) { + foreach (t; items) { + if (t.children.length > 0) + dropBraces(t.children); + } + for (int i=0; i= 0; i--) { + if (items[i].type != TokenType.Operator || items[i + 1].type != TokenType.Keyword) + continue; + if (items[i].operator == OperatorType.IS && items[i + 1].keyword == KeywordType.NULL) { + Token folded = new Token(items[i].pos,OperatorType.IS_NULL, "IS NULL"); + replaceInPlace(items, i, i + 2, [folded]); + i-=2; + } + } + for (int i = cast(int)items.length - 3; i >= 0; i--) { + if (items[i].type != TokenType.Operator || items[i + 1].type != TokenType.Operator || items[i + 2].type != TokenType.Keyword) + continue; + if (items[i].operator == OperatorType.IS && items[i + 1].operator == OperatorType.NOT && items[i + 2].keyword == KeywordType.NULL) { + Token folded = new Token(items[i].pos, OperatorType.IS_NOT_NULL, "IS NOT NULL"); + replaceInPlace(items, i, i + 3, [folded]); + i-=3; + } + } + } + + /** + * During the parsing of an HQL query, populates Tokens with TokenType Ident or Alias with + * contextual information such as EntityInfo, PropertyInfo, and more. + */ + void convertFields(ref Token[] items) { + while(true) { + // Find the first Ident/Alias token and keep its index in `p`. + int p = -1; + for (int i=0; i.] [.]... + // Extract only the Alias and Ident tokens, without Dot tokens, and store that in `idents`. + // Each dot represents either an @Embeddable property or a join. + string[] idents; + int lastp = p; + idents ~= items[p].text; + for (int i=p + 1; i < items.length - 1; i+=2) { + if (items[i].type != TokenType.Dot) + break; + enforceHelper!QuerySyntaxException( + i < items.length - 1 && items[i + 1].type == TokenType.Ident, + "Syntax error in property - no property name after . " ~ errorContext(items[p])); + lastp = i + 1; + idents ~= items[i + 1].text; + } + + // Determine the entity in the FromClause being referred to and make `idents` contain only Ident tokens. + FromClauseItem a; + if (items[p].type == TokenType.Alias) { + a = findFromClauseByAlias(idents[0]); + idents.popFront(); + } else { + // use first FROM clause if alias is not specified + a = fromClause.first; + } + string aliasName = a.entityAlias; + EntityInfo ei = cast(EntityInfo)a.entity; + enforceHelper!QuerySyntaxException(idents.length > 0, + "Syntax error in property - alias w/o property name: " ~ aliasName ~ errorContext(items[p])); + + // Dive through the list of identifiers, searching through embedded properties and joins. + PropertyInfo pi; + string fullName; + string columnPrefix; + fullName = aliasName; + while(true) { + string propertyName = idents[0]; + idents.popFront(); + fullName ~= "." ~ propertyName; + pi = cast(PropertyInfo)ei.findProperty(propertyName); + + // First consume all the dot-separated identifiers for @Embedded properties. + while (pi.embedded) { // loop to allow nested @Embedded + enforceHelper!QuerySyntaxException( + idents.length > 0, + "Syntax error in property - @Embedded property reference should include reference to @Embeddable property " + ~ aliasName ~ errorContext(items[p])); + // The `columnName` of an `@Embedded` property contains an optional prefix to add to the + // column names of the properties inside its entity type. + columnPrefix ~= pi.columnName == "" ? pi.columnName : pi.columnName ~ "_"; + propertyName = idents[0]; + idents.popFront(); + pi = cast(PropertyInfo)pi.referencedEntity.findProperty(propertyName); + fullName = fullName ~ "." ~ propertyName; + } + if (idents.length == 0) + break; + + // Next consume all the dot-separated identifiers for explicit or implicit joins. + if (idents.length > 0) { + // There are more names after `propertyName`, whose property info is in `pi`. + string pname = idents[0]; + enforceHelper!QuerySyntaxException(pi.referencedEntity !is null, "Unexpected extra field name " ~ pname ~ " - property " ~ propertyName ~ " doesn't content subproperties " ~ errorContext(items[p])); + ei = cast(EntityInfo)pi.referencedEntity; // Check if the referenced entity is already in a from-clause, if not, implicitly add it. - FromClauseItem newClause = fromClause.findByPath(fullName); - if (newClause is null) { - // autogenerate FROM clause - newClause = fromClause.add(ei, null, pi.nullable ? JoinType.LeftJoin : JoinType.InnerJoin, false, a, pi); - } - a = newClause; - } - } - enforceHelper!QuerySyntaxException(idents.length == 0, "Unexpected extra field name " ~ idents[0] ~ errorContext(items[p])); - //trace("full name = " ~ fullName); - - // Replace a sequence of tokens in `items[p..lastp+1]` of the form: - // [.] [.]... - // with a single Field token, containing the EntityInfo and PropertyInfo of the referenced entity and property. - Token t = new Token(/+pos+/ items[p].pos, /+type+/ TokenType.Field, /+text+/ fullName); - t.entity = cast(EntityInfo)ei; - t.field = cast(PropertyInfo)pi; - t.columnPrefix = columnPrefix; - t.from = a; - replaceInPlace(items, p, lastp + 1, [t]); - } - } - - static void convertUnaryPlusMinus(ref Token[] items) { - foreach (t; items) { - if (t.children.length > 0) - convertUnaryPlusMinus(t.children); - } - for (int i=0; i start, "Empty item in comma separated list" ~ errorContext(items[i])); - enforceHelper!QuerySyntaxException(i != items.length - 1, "Empty item in comma separated list" ~ errorContext(items[i])); - Token item = new Token(items[start].pos, TokenType.Expression, braces.children, start, i); - foldOperators(item.children); - enforceHelper!QuerySyntaxException(item.children.length == 1, "Invalid expression in list item" ~ errorContext(items[i])); - list ~= item.children[0]; - start = i + 1; - } - } - enforceHelper!QuerySyntaxException(list.length > 0, "Empty list" ~ errorContext(items[0])); - braces.type = TokenType.CommaDelimitedList; - braces.children = list; - } - - void foldOperators(ref Token[] items) { - foreach (t; items) { - if (t.children.length > 0) - foldOperators(t.children); - } - while (true) { - // - int bestOpPosition = -1; - int bestOpPrecedency = -1; - OperatorType t = OperatorType.NONE; - for (int i=0; i bestOpPrecedency) { - bestOpPrecedency = p; - bestOpPosition = i; - t = items[i].operator; - } - } - if (bestOpPrecedency == -1) - return; - //trace("Found op " ~ items[bestOpPosition].toString() ~ " at position " ~ to!string(bestOpPosition) ~ " with priority " ~ to!string(bestOpPrecedency)); - if (t == OperatorType.NOT || t == OperatorType.UNARY_PLUS || t == OperatorType.UNARY_MINUS) { - // fold unary - enforceHelper!QuerySyntaxException(bestOpPosition < items.length && items[bestOpPosition + 1].isExpression(), "Syntax error in WHERE condition " ~ errorContext(items[bestOpPosition])); - Token folded = new Token(items[bestOpPosition].pos, t, items[bestOpPosition].text, items[bestOpPosition + 1]); - replaceInPlace(items, bestOpPosition, bestOpPosition + 2, [folded]); - } else if (t == OperatorType.IS_NULL || t == OperatorType.IS_NOT_NULL) { - // fold unary - enforceHelper!QuerySyntaxException(bestOpPosition > 0 && items[bestOpPosition - 1].isExpression(), "Syntax error in WHERE condition " ~ errorContext(items[bestOpPosition])); - Token folded = new Token(items[bestOpPosition - 1].pos, t, items[bestOpPosition].text, items[bestOpPosition - 1]); - replaceInPlace(items, bestOpPosition - 1, bestOpPosition + 1, [folded]); - } else if (t == OperatorType.BETWEEN) { - // fold X BETWEEN A AND B - enforceHelper!QuerySyntaxException(bestOpPosition > 0, "Syntax error in WHERE condition - no left arg for BETWEEN operator"); - enforceHelper!QuerySyntaxException(bestOpPosition < items.length - 1, "Syntax error in WHERE condition - no min bound for BETWEEN operator " ~ errorContext(items[bestOpPosition])); - enforceHelper!QuerySyntaxException(bestOpPosition < items.length - 3, "Syntax error in WHERE condition - no max bound for BETWEEN operator " ~ errorContext(items[bestOpPosition])); - enforceHelper!QuerySyntaxException(items[bestOpPosition + 2].operator == OperatorType.AND, "Syntax error in WHERE condition - no max bound for BETWEEN operator" ~ errorContext(items[bestOpPosition])); - Token folded = new Token(items[bestOpPosition - 1].pos, t, items[bestOpPosition].text, items[bestOpPosition - 1]); - folded.children ~= items[bestOpPosition + 1]; - folded.children ~= items[bestOpPosition + 3]; - replaceInPlace(items, bestOpPosition - 1, bestOpPosition + 4, [folded]); - } else if (t == OperatorType.IN) { - // fold X IN (A, B, ...) - enforceHelper!QuerySyntaxException(bestOpPosition > 0, "Syntax error in WHERE condition - no left arg for IN operator"); - enforceHelper!QuerySyntaxException(bestOpPosition < items.length - 1, "Syntax error in WHERE condition - no value list for IN operator " ~ errorContext(items[bestOpPosition])); - enforceHelper!QuerySyntaxException(items[bestOpPosition + 1].type == TokenType.Braces, "Syntax error in WHERE condition - no value list in braces for IN operator" ~ errorContext(items[bestOpPosition])); - Token folded = new Token(items[bestOpPosition - 1].pos, t, items[bestOpPosition].text, items[bestOpPosition - 1]); - folded.children ~= items[bestOpPosition + 1]; - foldCommaSeparatedList(items[bestOpPosition + 1]); - replaceInPlace(items, bestOpPosition - 1, bestOpPosition + 2, [folded]); - // fold value list - //trace("IN operator found: " ~ folded.dump(3)); - } else { - // fold binary - enforceHelper!QuerySyntaxException(bestOpPosition > 0, "Syntax error in WHERE condition - no left arg for binary operator " ~ errorContext(items[bestOpPosition])); - enforceHelper!QuerySyntaxException(bestOpPosition < items.length - 1, "Syntax error in WHERE condition - no right arg for binary operator " ~ errorContext(items[bestOpPosition])); - //trace("binary op " ~ items[bestOpPosition - 1].toString() ~ " " ~ items[bestOpPosition].toString() ~ " " ~ items[bestOpPosition + 1].toString()); - enforceHelper!QuerySyntaxException(items[bestOpPosition - 1].isExpression(), "Syntax error in WHERE condition - wrong type of left arg for binary operator " ~ errorContext(items[bestOpPosition])); - enforceHelper!QuerySyntaxException(items[bestOpPosition + 1].isExpression(), "Syntax error in WHERE condition - wrong type of right arg for binary operator " ~ errorContext(items[bestOpPosition])); - Token folded = new Token(items[bestOpPosition - 1].pos, t, items[bestOpPosition].text, items[bestOpPosition - 1], items[bestOpPosition + 1]); - auto oldlen = items.length; - replaceInPlace(items, bestOpPosition - 1, bestOpPosition + 2, [folded]); - assert(items.length == oldlen - 2); - } - } - } - - void parseOrderClause(int start, int end) { - enforceHelper!QuerySyntaxException(start < end, "Invalid ORDER BY clause" ~ errorContext(tokens[start])); - trace("tokens[start..end]=", tokens[start..end].to!string); - splitCommaDelimitedList(start, end, &parseOrderByClauseItem); - } - - /// returns position of keyword in tokens array, -1 if not found - int findKeyword(KeywordType k, int startFrom = 0) { - for (int i = startFrom; i < tokens.length; i++) { - if (tokens[i].type == TokenType.Keyword && tokens[i].keyword == k) - return i; - } - return -1; - } - - int addSelectSQL(Dialect dialect, ParsedQuery res, string tableName, bool first, - const EntityInfo ei, string prefix="") { - int colCount = 0; - for(int j = 0; j < ei.getPropertyCount(); j++) { - PropertyInfo f = cast(PropertyInfo)ei.getProperty(j); - string fieldName = prefix ~ f.columnName; - if (f.embedded) { - // put embedded cols here - colCount += addSelectSQL(dialect, res, tableName, first && colCount == 0, f.referencedEntity, - /*prefix*/ fieldName == "" ? "" : fieldName ~ "_"); - continue; - } else if (f.oneToOne) { - } else { - } - if (fieldName is null) - continue; - if (!first || colCount > 0) { - res.appendSQL(", "); - } else - first = false; - - res.appendSQL(tableName ~ "." ~ dialect.quoteIfNeeded(fieldName)); - colCount++; - } - return colCount; - } - - void addSelectSQL(Dialect dialect, ParsedQuery res) { - res.appendSQL("SELECT "); - bool first = true; - assert(selectClause.length > 0); - int colCount = 0; - foreach(i, s; selectClause) { - s.from.selectIndex = cast(int)i; - } - if (selectClause[0].prop is null) { - // object alias is specified: add all properties of object - //trace("selected entity count: " ~ to!string(selectClause.length)); - res.setEntity(selectClause[0].from.entity); - for(int i = 0; i < fromClause.length; i++) { - FromClauseItem from = fromClause[i]; - if (!from.fetch) - continue; - string tableName = from.sqlAlias; - assert(from !is null); - assert(from.entity !is null); - colCount += addSelectSQL(dialect, res, tableName, colCount == 0, from.entity); - } - } else { - // individual fields specified - res.setEntity(null); - foreach(a; selectClause) { - string fieldName = a.prop.columnName; - string tableName = a.from.sqlAlias; - if (!first) { - res.appendSQL(", "); - } else - first = false; - res.appendSQL(tableName ~ "." ~ dialect.quoteIfNeeded(fieldName)); - colCount++; - } - } - res.setColCount(colCount); - res.setSelect(selectClause); - } - - void addFromSQL(Dialect dialect, ParsedQuery res) { - res.setFromClause(fromClause); - res.appendSpace(); - res.appendSQL("FROM "); - res.appendSQL(dialect.quoteIfNeeded(fromClause.first.entity.tableName) ~ " AS " ~ fromClause.first.sqlAlias); - for (int i = 1; i < fromClause.length; i++) { - FromClauseItem join = fromClause[i]; - FromClauseItem base = join.base; - assert(join !is null && base !is null); - res.appendSpace(); - - assert(join.baseProperty !is null); - if (join.baseProperty.manyToMany) { - string joinTableAlias = base.sqlAlias ~ join.sqlAlias; - res.appendSQL(join.joinType == JoinType.LeftJoin ? "LEFT JOIN " : "INNER JOIN "); - - res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.joinTable.tableName) ~ " AS " ~ joinTableAlias); - res.appendSQL(" ON "); - res.appendSQL(base.sqlAlias); - res.appendSQL("."); - res.appendSQL(dialect.quoteIfNeeded(base.entity.getKeyProperty().columnName)); - res.appendSQL("="); - res.appendSQL(joinTableAlias); - res.appendSQL("."); - res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.joinTable.column1)); - - res.appendSpace(); - - res.appendSQL(join.joinType == JoinType.LeftJoin ? "LEFT JOIN " : "INNER JOIN "); - res.appendSQL(dialect.quoteIfNeeded(join.entity.tableName) ~ " AS " ~ join.sqlAlias); - res.appendSQL(" ON "); - res.appendSQL(joinTableAlias); - res.appendSQL("."); - res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.joinTable.column2)); - res.appendSQL("="); - res.appendSQL(join.sqlAlias); - res.appendSQL("."); - res.appendSQL(dialect.quoteIfNeeded(join.entity.getKeyProperty().columnName)); - } else { - res.appendSQL(join.joinType == JoinType.LeftJoin ? "LEFT JOIN " : "INNER JOIN "); - res.appendSQL(dialect.quoteIfNeeded(join.entity.tableName) ~ " AS " ~ join.sqlAlias); - res.appendSQL(" ON "); - //trace("adding ON"); - if (join.baseProperty.oneToOne) { - assert(join.baseProperty.columnName !is null || join.baseProperty.referencedProperty !is null); - if (join.baseProperty.columnName !is null) { - //trace("fk is in base"); - res.appendSQL(base.sqlAlias); - res.appendSQL("."); - res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.columnName)); - res.appendSQL("="); - res.appendSQL(join.sqlAlias); - res.appendSQL("."); - res.appendSQL(dialect.quoteIfNeeded(join.entity.getKeyProperty().columnName)); - } else { - //trace("fk is in join"); - res.appendSQL(base.sqlAlias); - res.appendSQL("."); - res.appendSQL(dialect.quoteIfNeeded(base.entity.getKeyProperty().columnName)); - res.appendSQL("="); - res.appendSQL(join.sqlAlias); - res.appendSQL("."); - res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.referencedProperty.columnName)); - } - } else if (join.baseProperty.manyToOne) { - assert(join.baseProperty.columnName !is null, "ManyToOne should have JoinColumn as well"); - //trace("fk is in base"); - res.appendSQL(base.sqlAlias); - res.appendSQL("."); - res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.columnName)); - res.appendSQL("="); - res.appendSQL(join.sqlAlias); - res.appendSQL("."); - res.appendSQL(dialect.quoteIfNeeded(join.entity.getKeyProperty().columnName)); - } else if (join.baseProperty.oneToMany) { - res.appendSQL(base.sqlAlias); - res.appendSQL("."); - res.appendSQL(dialect.quoteIfNeeded(base.entity.getKeyProperty().columnName)); - res.appendSQL("="); - res.appendSQL(join.sqlAlias); - res.appendSQL("."); - res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.referencedProperty.columnName)); - } else { - // TODO: support other relations - throw new QuerySyntaxException("Invalid relation type in join"); - } - } - } - } - - // Converts a token into SQL and appends it to a WHERE section of a query. - void addWhereCondition(Token t, int basePrecedency, Dialect dialect, ParsedQuery res) { - if (t.type == TokenType.Expression) { - addWhereCondition(t.children[0], basePrecedency, dialect, res); - } else if (t.type == TokenType.Field) { - string tableName = t.from.sqlAlias; - string fieldName = t.columnPrefix ~ t.field.columnName; - res.appendSpace(); - res.appendSQL(tableName ~ "." ~ dialect.quoteIfNeeded(fieldName)); - } else if (t.type == TokenType.Number) { - res.appendSpace(); - res.appendSQL(t.text); - } else if (t.type == TokenType.String) { - res.appendSpace(); - res.appendSQL(dialect.quoteSqlString(t.text)); - } else if (t.type == TokenType.Parameter) { - res.appendSpace(); - res.appendSQL("?"); - res.addParam(t.text); - } else if (t.type == TokenType.CommaDelimitedList) { - bool first = true; - for (int i=0; i - LT, // < - GT, // > - LE, // <= - GE, // >= - MUL,// * - ADD,// + - SUB,// - - DIV,// / - - // from keywords - LIKE, - IN, - IS, - NOT, - AND, - OR, - BETWEEN, - IDIV, - MOD, - - UNARY_PLUS, - UNARY_MINUS, - - IS_NULL, - IS_NOT_NULL, -} - -OperatorType isOperator(KeywordType t) { - switch (t) { - case KeywordType.LIKE: return OperatorType.LIKE; - case KeywordType.IN: return OperatorType.IN; - case KeywordType.IS: return OperatorType.IS; - case KeywordType.NOT: return OperatorType.NOT; - case KeywordType.AND: return OperatorType.AND; - case KeywordType.OR: return OperatorType.OR; - case KeywordType.BETWEEN: return OperatorType.BETWEEN; - case KeywordType.DIV: return OperatorType.IDIV; - case KeywordType.MOD: return OperatorType.MOD; - default: return OperatorType.NONE; - } -} - -int operatorPrecedency(OperatorType t) { - switch(t) { - case OperatorType.EQ: return 5; // == - case OperatorType.NE: return 5; // != <> - case OperatorType.LT: return 5; // < - case OperatorType.GT: return 5; // > - case OperatorType.LE: return 5; // <= - case OperatorType.GE: return 5; // >= - case OperatorType.MUL: return 10; // * - case OperatorType.ADD: return 9; // + - case OperatorType.SUB: return 9; // - - case OperatorType.DIV: return 10; // / - // from keywords - case OperatorType.LIKE: return 11; - case OperatorType.IN: return 12; - case OperatorType.IS: return 13; - case OperatorType.NOT: return 6; // ??? - case OperatorType.AND: return 4; - case OperatorType.OR: return 3; - case OperatorType.BETWEEN: return 7; // ??? - case OperatorType.IDIV: return 10; - case OperatorType.MOD: return 10; - case OperatorType.UNARY_PLUS: return 15; - case OperatorType.UNARY_MINUS: return 15; - case OperatorType.IS_NULL: return 15; - case OperatorType.IS_NOT_NULL: return 15; - default: return -1; - } -} - -OperatorType isOperator(string s, ref int i) { - int len = cast(int)s.length; - char ch = s[i]; - char ch2 = i < len - 1 ? s[i + 1] : 0; - //char ch3 = i < len - 2 ? s[i + 2] : 0; - if (ch == '=' && ch2 == '=') { i++; return OperatorType.EQ; } // == - if (ch == '!' && ch2 == '=') { i++; return OperatorType.NE; } // != - if (ch == '<' && ch2 == '>') { i++; return OperatorType.NE; } // <> - if (ch == '<' && ch2 == '=') { i++; return OperatorType.LE; } // <= - if (ch == '>' && ch2 == '=') { i++; return OperatorType.GE; } // >= - if (ch == '=') return OperatorType.EQ; // = - if (ch == '<') return OperatorType.LT; // < - if (ch == '>') return OperatorType.GT; // < - if (ch == '*') return OperatorType.MUL; // < - if (ch == '+') return OperatorType.ADD; // < - if (ch == '-') return OperatorType.SUB; // < - if (ch == '/') return OperatorType.DIV; // < - return OperatorType.NONE; -} - - -enum TokenType { - Keyword, // WHERE - Ident, // ident - Number, // 25 13.5e-10 - String, // 'string' - Operator, // == != <= >= < > + - * / - Dot, // . - OpenBracket, // ( - CloseBracket, // ) - Comma, // , - Entity, // entity name - Field, // field name of some entity - Alias, // alias name of some entity - Parameter, // ident after : - // types of compound AST nodes - Expression, // any expression - Braces, // ( tokens ) - CommaDelimitedList, // tokens, ... , tokens - OpExpr, // operator expression; current token == operator, children = params -} - -class Token { - int pos; - TokenType type; - KeywordType keyword = KeywordType.NONE; - OperatorType operator = OperatorType.NONE; - string text; - string spaceAfter; - EntityInfo entity; - PropertyInfo field; - FromClauseItem from; - // Embedded fields may have a prefix derived from `@Embedded` annotations on properties that - // contain them. - string columnPrefix; - Token[] children; - this(int pos, TokenType type, string text) { - this.pos = pos; - this.type = type; - this.text = text; - } - this(int pos, KeywordType keyword, string text) { - this.pos = pos; - this.type = TokenType.Keyword; - this.keyword = keyword; - this.text = text; - } - this(int pos, OperatorType op, string text) { - this.pos = pos; - this.type = TokenType.Operator; - this.operator = op; - this.text = text; - } - this(int pos, TokenType type, Token[] base, int start, int end) { - this.pos = pos; - this.type = type; - this.children = new Token[end - start]; - for (int i = start; i < end; i++) - children[i - start] = base[i]; - } - // unary operator expression - this(int pos, OperatorType type, string text, Token right) { - this.pos = pos; - this.type = TokenType.OpExpr; - this.operator = type; - this.text = text; - this.children = new Token[1]; - this.children[0] = right; - } - // binary operator expression - this(int pos, OperatorType type, string text, Token left, Token right) { - this.pos = pos; - this.type = TokenType.OpExpr; - this.text = text; - this.operator = type; - this.children = new Token[2]; - this.children[0] = left; - this.children[1] = right; - } - bool isExpression() { - return type==TokenType.Expression || type==TokenType.Braces || type==TokenType.OpExpr || type==TokenType.Parameter - || type==TokenType.Field || type==TokenType.String || type==TokenType.Number; - } - bool isCompound() { - return this.type >= TokenType.Expression; - } - string dump(int level) { - string res; - for (int i=0; i= < > + - * / - case TokenType.Dot: return "."; // . - case TokenType.OpenBracket: return "("; // ( - case TokenType.CloseBracket: return ")"; // ) - case TokenType.Comma: return ","; // , - case TokenType.Entity: return "entity: " ~ entity.name; // entity name - case TokenType.Field: return from.entityAlias ~ "." ~ field.propertyName; // field name of some entity - case TokenType.Alias: return "alias: " ~ text; // alias name of some entity - case TokenType.Parameter: return ":" ~ text; // ident after : - // types of compound AST nodes - case TokenType.Expression: return "expr"; // any expression - case TokenType.Braces: return "()"; // ( tokens ) - case TokenType.CommaDelimitedList: return ",,,"; // tokens, ... , tokens - case TokenType.OpExpr: return "" ~ text; - default: return "UNKNOWN"; - } - } - -} - -Token[] tokenize(string s) { - Token[] res; - int startpos = 0; - int state = 0; - int len = cast(int)s.length; - for (int i=0; i 0, "Invalid parameter name near " ~ cast(string)s[startpos .. $]); - res ~= new Token(startpos, TokenType.Parameter, text); - } else if (isAlpha(ch) || ch=='_' || quotedIdent) { - // identifier or keyword - if (quotedIdent) { - i++; - enforceHelper!QuerySyntaxException(i < len - 1, "Invalid quoted identifier near " ~ cast(string)s[startpos .. $]); - } - // && state == 0 - for(int j=i; j 0, "Invalid quoted identifier near " ~ cast(string)s[startpos .. $]); - if (quotedIdent) { - enforceHelper!QuerySyntaxException(i < len - 1 && s[i + 1] == '`', "Invalid quoted identifier near " ~ cast(string)s[startpos .. $]); - i++; - } - KeywordType keywordId = isKeyword(text); - if (keywordId != KeywordType.NONE && !quotedIdent) { - OperatorType keywordOp = isOperator(keywordId); - if (keywordOp != OperatorType.NONE) - res ~= new Token(startpos, keywordOp, text); // operator keyword - else - res ~= new Token(startpos, keywordId, text); - } else - res ~= new Token(startpos, TokenType.Ident, text); - } else if (isWhite(ch)) { - // whitespace - for(int j=i; j 0) { - res[$ - 1].spaceAfter = text; - } - } else if (ch == '\'') { - // string constant - i++; - for(int j=i; j= len - 1 || !isAlpha(s[i]), "Invalid number near " ~ cast(string)s[startpos .. $]); - res ~= new Token(startpos, TokenType.Number, text); - } else if (ch == '.') { - res ~= new Token(startpos, TokenType.Dot, "."); - } else if (ch == '(') { - res ~= new Token(startpos, TokenType.OpenBracket, "("); - } else if (ch == ')') { - res ~= new Token(startpos, TokenType.CloseBracket, ")"); - } else if (ch == ',') { - res ~= new Token(startpos, TokenType.Comma, ","); - } else { - enforceHelper!QuerySyntaxException(false, "Invalid character near " ~ cast(string)s[startpos .. $]); - } - } - return res; -} - -unittest { - Token[] tokens; - tokens = tokenize("SELECT a From User a where a.flags = 12 AND a.name='john' ORDER BY a.idx ASC"); - assert(tokens.length == 23); - assert(tokens[0].type == TokenType.Keyword); - assert(tokens[2].type == TokenType.Keyword); - assert(tokens[5].type == TokenType.Keyword); - assert(tokens[5].text == "where"); - assert(tokens[10].type == TokenType.Number); - assert(tokens[10].text == "12"); - assert(tokens[16].type == TokenType.String); - assert(tokens[16].text == "john"); - assert(tokens[22].type == TokenType.Keyword); - assert(tokens[22].text == "ASC"); -} - -class ParameterValues { - Variant[string] values; - int[][string]params; - int[string]unboundParams; - this(int[][string]params) { - this.params = params; - foreach(key, value; params) { - unboundParams[key] = 1; - } - } - void setParameter(string name, Variant value) { - enforceHelper!QueryParameterException((name in params) !is null, "Attempting to set unknown parameter " ~ name); - unboundParams.remove(name); - values[name] = value; - } - void checkAllParametersSet() { - if (unboundParams.length == 0) - return; - string list; - foreach(key, value; unboundParams) { - if (list.length > 0) - list ~= ", "; - list ~= key; - } - enforceHelper!QueryParameterException(false, "Parameters " ~ list ~ " not set"); - } - void applyParams(DataSetWriter ds) { - foreach(key, indexes; params) { - Variant value = values[key]; - foreach(i; indexes) - ds.setVariant(i, value); - } - } -} - -class ParsedQuery { - private string _hql; - private string _sql; - private int[][string]params; // contains 1-based indexes of ? ? ? placeholders in SQL for param by name - private int paramIndex = 1; - private FromClause _from; - private SelectClauseItem[] _select; - private EntityInfo _entity; - private int _colCount = 0; - this(string hql) { - _hql = hql; - } - @property string hql() { return _hql; } - @property string sql() { return _sql; } - @property const(EntityInfo)entity() { return _entity; } - @property int colCount() { return _colCount; } - @property FromClause from() { return _from; } - @property SelectClauseItem[] select() { return _select; } - void setEntity(const EntityInfo entity) { - _entity = cast(EntityInfo)entity; - } - void setFromClause(FromClause from) { - _from = from; - } - void setSelect(SelectClauseItem[] items) { - _select = items; - } - void setColCount(int cnt) { _colCount = cnt; } - void addParam(string paramName) { - if ((paramName in params) is null) { - params[paramName] = [paramIndex++]; - } else { - params[paramName] ~= [paramIndex++]; - } - } - int[] getParam(string paramName) { - if ((paramName in params) is null) { - throw new HibernatedException("Parameter " ~ paramName ~ " not found in query " ~ _hql); - } else { - return params[paramName]; - } - } - void appendSQL(string sql) { - _sql ~= sql; - } - void appendSpace() { - if (_sql.length > 0 && _sql[$ - 1] != ' ') - _sql ~= ' '; - } - ParameterValues createParams() { - return new ParameterValues(params); - } -} - -unittest { - ParsedQuery q = new ParsedQuery("FROM User where id = :param1 or id = :param2"); - q.addParam("param1"); // 1 - q.addParam("param2"); // 2 - q.addParam("param1"); // 3 - q.addParam("param1"); // 4 - q.addParam("param3"); // 5 - q.addParam("param2"); // 6 - assert(q.getParam("param1") == [1,3,4]); - assert(q.getParam("param2") == [2,6]); - assert(q.getParam("param3") == [5]); -} - -unittest { - - //trace("query unittest"); - import hibernated.tests; - - EntityMetaData schema = new SchemaInfoImpl!(User, Customer, AccountType, Address, Person, MoreInfo, EvenMoreInfo, Role); - 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"); - assert(parser.parameterNames.length == 2); - //trace("param1=" ~ parser.parameterNames[0]); - //trace("param2=" ~ parser.parameterNames[1]); - assert(parser.parameterNames[0] == "Id"); - assert(parser.parameterNames[1] == "skipName"); - assert(parser.fromClause.length == 1); - assert(parser.fromClause.first.entity.name == "User"); - assert(parser.fromClause.first.entityAlias == "a"); - assert(parser.selectClause.length == 1); - assert(parser.selectClause[0].prop is null); - assert(parser.selectClause[0].from.entity.name == "User"); - assert(parser.orderByClause.length == 2); - assert(parser.orderByClause[0].prop.propertyName == "name"); - assert(parser.orderByClause[0].from.entity.name == "User"); - assert(parser.orderByClause[0].asc == true); - assert(parser.orderByClause[1].prop.propertyName == "flags"); - assert(parser.orderByClause[1].from.entity.name == "User"); - assert(parser.orderByClause[1].asc == false); - - 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"); - assert(parser.whereClause !is null); - //trace(parser.whereClause.dump(0)); - Dialect dialect = new MySQLDialect(); - - assert(dialect.quoteSqlString("abc") == "'abc'"); - assert(dialect.quoteSqlString("a'b'c") == "'a\\'b\\'c'"); - assert(dialect.quoteSqlString("a\nc") == "'a\\nc'"); - - parser = new QueryParser(schema, "FROM User AS u WHERE id = :Id and u.name like '%test%'"); - ParsedQuery q = parser.makeSQL(dialect); - //trace(parser.whereClause.dump(0)); - //trace(q.hql ~ "\n=>\n" ~ q.sql); - - //trace(q.hql); - //trace(q.sql); - 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"); - assert(parser.fromClause.hasAlias("a")); - assert(parser.fromClause.hasAlias("b")); - assert(parser.fromClause.findByAlias("a").entityName == "Person"); - assert(parser.fromClause.findByAlias("b").entityName == "MoreInfo"); - assert(parser.fromClause.findByAlias("b").joinType == JoinType.LeftJoin); - assert(parser.fromClause.findByAlias("c").entityName == "EvenMoreInfo"); - // indirect JOIN - parser = new QueryParser(schema, "SELECT a FROM Person a WHERE a.id = :Id AND a.moreInfo.evenMore.flags > 0"); - assert(parser.fromClause.hasAlias("a")); - assert(parser.fromClause.length == 3); - assert(parser.fromClause[0].entity.tableName == "person"); - assert(parser.fromClause[1].entity.tableName == "person_info"); - assert(parser.fromClause[1].joinType == JoinType.InnerJoin); - assert(parser.fromClause[1].pathString == "a.moreInfo"); - assert(parser.fromClause[2].entity.tableName == "person_info2"); - assert(parser.fromClause[2].joinType == JoinType.LeftJoin); - assert(parser.fromClause[2].pathString == "a.moreInfo.evenMore"); - // indirect JOIN, no alias - parser = new QueryParser(schema, "FROM Person WHERE id = :Id AND moreInfo.evenMore.flags > 0"); - assert(parser.fromClause.length == 3); - assert(parser.fromClause[0].entity.tableName == "person"); - assert(parser.fromClause[0].fetch == true); - //trace("select fields [" ~ to!string(parser.fromClause[0].startColumn) ~ ", " ~ to!string(parser.fromClause[0].selectedColumns) ~ "]"); - //trace("select fields [" ~ to!string(parser.fromClause[1].startColumn) ~ ", " ~ to!string(parser.fromClause[1].selectedColumns) ~ "]"); - //trace("select fields [" ~ to!string(parser.fromClause[2].startColumn) ~ ", " ~ to!string(parser.fromClause[2].selectedColumns) ~ "]"); - assert(parser.fromClause[0].selectedColumns == 4); - assert(parser.fromClause[1].entity.tableName == "person_info"); - assert(parser.fromClause[1].joinType == JoinType.InnerJoin); - assert(parser.fromClause[1].pathString == "_a1.moreInfo"); - assert(parser.fromClause[1].fetch == true); - assert(parser.fromClause[1].selectedColumns == 2); - assert(parser.fromClause[2].entity.tableName == "person_info2"); - assert(parser.fromClause[2].joinType == JoinType.LeftJoin); - assert(parser.fromClause[2].pathString == "_a1.moreInfo.evenMore"); - assert(parser.fromClause[2].fetch == true); - assert(parser.fromClause[2].selectedColumns == 3); - - q = parser.makeSQL(dialect); - //trace(q.hql); - //trace(q.sql); - - parser = new QueryParser(schema, "FROM User WHERE id in (1, 2, (3 - 1 * 25) / 2, 4 + :Id, 5)"); - //trace(parser.whereClause.dump(0)); - q = parser.makeSQL(dialect); - //trace(q.hql); - //trace(q.sql); - - parser = new QueryParser(schema, "FROM Customer WHERE users.id = 1"); - q = parser.makeSQL(dialect); -// trace(q.hql); -// trace(q.sql); - 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"); - - parser = new QueryParser(schema, "FROM Customer WHERE id = 1"); - q = parser.makeSQL(dialect); -// trace(q.hql); -// trace(q.sql); - 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"); - - parser = new QueryParser(schema, "FROM User WHERE roles.id = 1"); - q = parser.makeSQL(dialect); - //trace(q.hql); - //trace(q.sql); - 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"); - - parser = new QueryParser(schema, "FROM Role WHERE users.id = 1"); - q = parser.makeSQL(dialect); -// trace(q.hql); -// trace(q.sql); - 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"); - - parser = new QueryParser(schema, "FROM User WHERE customer.id = 1"); - q = parser.makeSQL(dialect); -// trace(q.hql); -// trace(q.sql); - 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"); - - parser = new QueryParser(schema, "SELECT a2 FROM User AS a1 JOIN a1.roles AS a2 WHERE a1.id = 1"); - q = parser.makeSQL(dialect); - //trace(q.hql); - //trace(q.sql); - 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"); - - parser = new QueryParser(schema, "SELECT a2 FROM Customer AS a1 JOIN a1.users AS a2 WHERE a1.id = 1"); - q = parser.makeSQL(dialect); - //trace(q.hql); - //trace(q.sql); - 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"); - -} + FromClauseItem newClause = fromClause.findByPath(fullName); + if (newClause is null) { + // autogenerate FROM clause + newClause = fromClause.add(ei, null, pi.nullable ? JoinType.LeftJoin : JoinType.InnerJoin, false, a, pi); + } + a = newClause; + } + } + enforceHelper!QuerySyntaxException(idents.length == 0, "Unexpected extra field name " ~ idents[0] ~ errorContext(items[p])); + //trace("full name = " ~ fullName); + + // Replace a sequence of tokens in `items[p..lastp+1]` of the form: + // [.] [.]... + // with a single Field token, containing the EntityInfo and PropertyInfo of the referenced entity and property. + Token t = new Token(/+pos+/ items[p].pos, /+type+/ TokenType.Field, /+text+/ fullName); + t.entity = cast(EntityInfo)ei; + t.field = cast(PropertyInfo)pi; + t.columnPrefix = columnPrefix; + t.from = a; + replaceInPlace(items, p, lastp + 1, [t]); + } + } + + static void convertUnaryPlusMinus(ref Token[] items) { + foreach (t; items) { + if (t.children.length > 0) + convertUnaryPlusMinus(t.children); + } + for (int i=0; i start, "Empty item in comma separated list" ~ errorContext(items[i])); + enforceHelper!QuerySyntaxException(i != items.length - 1, "Empty item in comma separated list" ~ errorContext(items[i])); + Token item = new Token(items[start].pos, TokenType.Expression, braces.children, start, i); + foldOperators(item.children); + enforceHelper!QuerySyntaxException(item.children.length == 1, "Invalid expression in list item" ~ errorContext(items[i])); + list ~= item.children[0]; + start = i + 1; + } + } + enforceHelper!QuerySyntaxException(list.length > 0, "Empty list" ~ errorContext(items[0])); + braces.type = TokenType.CommaDelimitedList; + braces.children = list; + } + + void foldOperators(ref Token[] items) { + foreach (t; items) { + if (t.children.length > 0) + foldOperators(t.children); + } + while (true) { + // + int bestOpPosition = -1; + int bestOpPrecedency = -1; + OperatorType t = OperatorType.NONE; + for (int i=0; i bestOpPrecedency) { + bestOpPrecedency = p; + bestOpPosition = i; + t = items[i].operator; + } + } + if (bestOpPrecedency == -1) + return; + //trace("Found op " ~ items[bestOpPosition].toString() ~ " at position " ~ to!string(bestOpPosition) ~ " with priority " ~ to!string(bestOpPrecedency)); + if (t == OperatorType.NOT || t == OperatorType.UNARY_PLUS || t == OperatorType.UNARY_MINUS) { + // fold unary + enforceHelper!QuerySyntaxException(bestOpPosition < items.length && items[bestOpPosition + 1].isExpression(), "Syntax error in WHERE condition " ~ errorContext(items[bestOpPosition])); + Token folded = new Token(items[bestOpPosition].pos, t, items[bestOpPosition].text, items[bestOpPosition + 1]); + replaceInPlace(items, bestOpPosition, bestOpPosition + 2, [folded]); + } else if (t == OperatorType.IS_NULL || t == OperatorType.IS_NOT_NULL) { + // fold unary + enforceHelper!QuerySyntaxException(bestOpPosition > 0 && items[bestOpPosition - 1].isExpression(), "Syntax error in WHERE condition " ~ errorContext(items[bestOpPosition])); + Token folded = new Token(items[bestOpPosition - 1].pos, t, items[bestOpPosition].text, items[bestOpPosition - 1]); + replaceInPlace(items, bestOpPosition - 1, bestOpPosition + 1, [folded]); + } else if (t == OperatorType.BETWEEN) { + // fold X BETWEEN A AND B + enforceHelper!QuerySyntaxException(bestOpPosition > 0, "Syntax error in WHERE condition - no left arg for BETWEEN operator"); + enforceHelper!QuerySyntaxException(bestOpPosition < items.length - 1, "Syntax error in WHERE condition - no min bound for BETWEEN operator " ~ errorContext(items[bestOpPosition])); + enforceHelper!QuerySyntaxException(bestOpPosition < items.length - 3, "Syntax error in WHERE condition - no max bound for BETWEEN operator " ~ errorContext(items[bestOpPosition])); + enforceHelper!QuerySyntaxException(items[bestOpPosition + 2].operator == OperatorType.AND, "Syntax error in WHERE condition - no max bound for BETWEEN operator" ~ errorContext(items[bestOpPosition])); + Token folded = new Token(items[bestOpPosition - 1].pos, t, items[bestOpPosition].text, items[bestOpPosition - 1]); + folded.children ~= items[bestOpPosition + 1]; + folded.children ~= items[bestOpPosition + 3]; + replaceInPlace(items, bestOpPosition - 1, bestOpPosition + 4, [folded]); + } else if (t == OperatorType.IN) { + // fold X IN (A, B, ...) + enforceHelper!QuerySyntaxException(bestOpPosition > 0, "Syntax error in WHERE condition - no left arg for IN operator"); + enforceHelper!QuerySyntaxException(bestOpPosition < items.length - 1, "Syntax error in WHERE condition - no value list for IN operator " ~ errorContext(items[bestOpPosition])); + enforceHelper!QuerySyntaxException(items[bestOpPosition + 1].type == TokenType.Braces, "Syntax error in WHERE condition - no value list in braces for IN operator" ~ errorContext(items[bestOpPosition])); + Token folded = new Token(items[bestOpPosition - 1].pos, t, items[bestOpPosition].text, items[bestOpPosition - 1]); + folded.children ~= items[bestOpPosition + 1]; + foldCommaSeparatedList(items[bestOpPosition + 1]); + replaceInPlace(items, bestOpPosition - 1, bestOpPosition + 2, [folded]); + // fold value list + //trace("IN operator found: " ~ folded.dump(3)); + } else { + // fold binary + enforceHelper!QuerySyntaxException(bestOpPosition > 0, "Syntax error in WHERE condition - no left arg for binary operator " ~ errorContext(items[bestOpPosition])); + enforceHelper!QuerySyntaxException(bestOpPosition < items.length - 1, "Syntax error in WHERE condition - no right arg for binary operator " ~ errorContext(items[bestOpPosition])); + //trace("binary op " ~ items[bestOpPosition - 1].toString() ~ " " ~ items[bestOpPosition].toString() ~ " " ~ items[bestOpPosition + 1].toString()); + enforceHelper!QuerySyntaxException(items[bestOpPosition - 1].isExpression(), "Syntax error in WHERE condition - wrong type of left arg for binary operator " ~ errorContext(items[bestOpPosition])); + enforceHelper!QuerySyntaxException(items[bestOpPosition + 1].isExpression(), "Syntax error in WHERE condition - wrong type of right arg for binary operator " ~ errorContext(items[bestOpPosition])); + Token folded = new Token(items[bestOpPosition - 1].pos, t, items[bestOpPosition].text, items[bestOpPosition - 1], items[bestOpPosition + 1]); + auto oldlen = items.length; + replaceInPlace(items, bestOpPosition - 1, bestOpPosition + 2, [folded]); + assert(items.length == oldlen - 2); + } + } + } + + void parseOrderClause(int start, int end) { + enforceHelper!QuerySyntaxException(start < end, "Invalid ORDER BY clause" ~ errorContext(tokens[start])); + trace("tokens[start..end]=", tokens[start..end].to!string); + splitCommaDelimitedList(start, end, &parseOrderByClauseItem); + } + + /// returns position of keyword in tokens array, -1 if not found + int findKeyword(KeywordType k, int startFrom = 0) { + for (int i = startFrom; i < tokens.length; i++) { + if (tokens[i].type == TokenType.Keyword && tokens[i].keyword == k) + return i; + } + return -1; + } + + int addSelectSQL(Dialect dialect, ParsedQuery res, string tableName, bool first, + const EntityInfo ei, string prefix="") { + int colCount = 0; + for(int j = 0; j < ei.getPropertyCount(); j++) { + PropertyInfo f = cast(PropertyInfo)ei.getProperty(j); + string fieldName = prefix ~ f.columnName; + if (f.embedded) { + // put embedded cols here + colCount += addSelectSQL(dialect, res, tableName, first && colCount == 0, f.referencedEntity, + /*prefix*/ fieldName == "" ? "" : fieldName ~ "_"); + continue; + } else if (f.oneToOne) { + } else { + } + if (fieldName is null) + continue; + if (!first || colCount > 0) { + res.appendSQL(", "); + } else + first = false; + + res.appendSQL(tableName ~ "." ~ dialect.quoteIfNeeded(fieldName)); + colCount++; + } + return colCount; + } + + void addSelectSQL(Dialect dialect, ParsedQuery res) { + res.appendSQL("SELECT "); + bool first = true; + assert(selectClause.length > 0); + int colCount = 0; + foreach(i, s; selectClause) { + s.from.selectIndex = cast(int)i; + } + if (selectClause[0].prop is null) { + // object alias is specified: add all properties of object + //trace("selected entity count: " ~ to!string(selectClause.length)); + res.setEntity(selectClause[0].from.entity); + for(int i = 0; i < fromClause.length; i++) { + FromClauseItem from = fromClause[i]; + if (!from.fetch) + continue; + string tableName = from.sqlAlias; + assert(from !is null); + assert(from.entity !is null); + colCount += addSelectSQL(dialect, res, tableName, colCount == 0, from.entity); + } + } else { + // individual fields specified + res.setEntity(null); + foreach(a; selectClause) { + string fieldName = a.prop.columnName; + string tableName = a.from.sqlAlias; + if (!first) { + res.appendSQL(", "); + } else + first = false; + res.appendSQL(tableName ~ "." ~ dialect.quoteIfNeeded(fieldName)); + colCount++; + } + } + res.setColCount(colCount); + res.setSelect(selectClause); + } + + void addFromSQL(Dialect dialect, ParsedQuery res) { + res.setFromClause(fromClause); + res.appendSpace(); + res.appendSQL("FROM "); + res.appendSQL(dialect.quoteIfNeeded(fromClause.first.entity.tableName) ~ " AS " ~ fromClause.first.sqlAlias); + for (int i = 1; i < fromClause.length; i++) { + FromClauseItem join = fromClause[i]; + FromClauseItem base = join.base; + assert(join !is null && base !is null); + res.appendSpace(); + + assert(join.baseProperty !is null); + if (join.baseProperty.manyToMany) { + string joinTableAlias = base.sqlAlias ~ join.sqlAlias; + res.appendSQL(join.joinType == JoinType.LeftJoin ? "LEFT JOIN " : "INNER JOIN "); + + res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.joinTable.tableName) ~ " AS " ~ joinTableAlias); + res.appendSQL(" ON "); + res.appendSQL(base.sqlAlias); + res.appendSQL("."); + res.appendSQL(dialect.quoteIfNeeded(base.entity.getKeyProperty().columnName)); + res.appendSQL("="); + res.appendSQL(joinTableAlias); + res.appendSQL("."); + res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.joinTable.column1)); + + res.appendSpace(); + + res.appendSQL(join.joinType == JoinType.LeftJoin ? "LEFT JOIN " : "INNER JOIN "); + res.appendSQL(dialect.quoteIfNeeded(join.entity.tableName) ~ " AS " ~ join.sqlAlias); + res.appendSQL(" ON "); + res.appendSQL(joinTableAlias); + res.appendSQL("."); + res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.joinTable.column2)); + res.appendSQL("="); + res.appendSQL(join.sqlAlias); + res.appendSQL("."); + res.appendSQL(dialect.quoteIfNeeded(join.entity.getKeyProperty().columnName)); + } else { + res.appendSQL(join.joinType == JoinType.LeftJoin ? "LEFT JOIN " : "INNER JOIN "); + res.appendSQL(dialect.quoteIfNeeded(join.entity.tableName) ~ " AS " ~ join.sqlAlias); + res.appendSQL(" ON "); + //trace("adding ON"); + if (join.baseProperty.oneToOne) { + assert(join.baseProperty.columnName !is null || join.baseProperty.referencedProperty !is null); + if (join.baseProperty.columnName !is null) { + //trace("fk is in base"); + res.appendSQL(base.sqlAlias); + res.appendSQL("."); + res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.columnName)); + res.appendSQL("="); + res.appendSQL(join.sqlAlias); + res.appendSQL("."); + res.appendSQL(dialect.quoteIfNeeded(join.entity.getKeyProperty().columnName)); + } else { + //trace("fk is in join"); + res.appendSQL(base.sqlAlias); + res.appendSQL("."); + res.appendSQL(dialect.quoteIfNeeded(base.entity.getKeyProperty().columnName)); + res.appendSQL("="); + res.appendSQL(join.sqlAlias); + res.appendSQL("."); + res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.referencedProperty.columnName)); + } + } else if (join.baseProperty.manyToOne) { + assert(join.baseProperty.columnName !is null, "ManyToOne should have JoinColumn as well"); + //trace("fk is in base"); + res.appendSQL(base.sqlAlias); + res.appendSQL("."); + res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.columnName)); + res.appendSQL("="); + res.appendSQL(join.sqlAlias); + res.appendSQL("."); + res.appendSQL(dialect.quoteIfNeeded(join.entity.getKeyProperty().columnName)); + } else if (join.baseProperty.oneToMany) { + res.appendSQL(base.sqlAlias); + res.appendSQL("."); + res.appendSQL(dialect.quoteIfNeeded(base.entity.getKeyProperty().columnName)); + res.appendSQL("="); + res.appendSQL(join.sqlAlias); + res.appendSQL("."); + res.appendSQL(dialect.quoteIfNeeded(join.baseProperty.referencedProperty.columnName)); + } else { + // TODO: support other relations + throw new QuerySyntaxException("Invalid relation type in join"); + } + } + } + } + + // Converts a token into SQL and appends it to a WHERE section of a query. + void addWhereCondition(Token t, int basePrecedency, Dialect dialect, ParsedQuery res) { + if (t.type == TokenType.Expression) { + addWhereCondition(t.children[0], basePrecedency, dialect, res); + } else if (t.type == TokenType.Field) { + string tableName = t.from.sqlAlias; + string fieldName = t.columnPrefix ~ t.field.columnName; + res.appendSpace(); + res.appendSQL(tableName ~ "." ~ dialect.quoteIfNeeded(fieldName)); + } else if (t.type == TokenType.Number) { + res.appendSpace(); + res.appendSQL(t.text); + } else if (t.type == TokenType.String) { + res.appendSpace(); + res.appendSQL(dialect.quoteSqlString(t.text)); + } else if (t.type == TokenType.Parameter) { + res.appendSpace(); + res.appendSQL("?"); + res.addParam(t.text); + } else if (t.type == TokenType.CommaDelimitedList) { + bool first = true; + for (int i=0; i + LT, // < + GT, // > + LE, // <= + GE, // >= + MUL,// * + ADD,// + + SUB,// - + DIV,// / + + // from keywords + LIKE, + IN, + IS, + NOT, + AND, + OR, + BETWEEN, + IDIV, + MOD, + + UNARY_PLUS, + UNARY_MINUS, + + IS_NULL, + IS_NOT_NULL, +} + +OperatorType isOperator(KeywordType t) { + switch (t) { + case KeywordType.LIKE: return OperatorType.LIKE; + case KeywordType.IN: return OperatorType.IN; + case KeywordType.IS: return OperatorType.IS; + case KeywordType.NOT: return OperatorType.NOT; + case KeywordType.AND: return OperatorType.AND; + case KeywordType.OR: return OperatorType.OR; + case KeywordType.BETWEEN: return OperatorType.BETWEEN; + case KeywordType.DIV: return OperatorType.IDIV; + case KeywordType.MOD: return OperatorType.MOD; + default: return OperatorType.NONE; + } +} + +int operatorPrecedency(OperatorType t) { + switch(t) { + case OperatorType.EQ: return 5; // == + case OperatorType.NE: return 5; // != <> + case OperatorType.LT: return 5; // < + case OperatorType.GT: return 5; // > + case OperatorType.LE: return 5; // <= + case OperatorType.GE: return 5; // >= + case OperatorType.MUL: return 10; // * + case OperatorType.ADD: return 9; // + + case OperatorType.SUB: return 9; // - + case OperatorType.DIV: return 10; // / + // from keywords + case OperatorType.LIKE: return 11; + case OperatorType.IN: return 12; + case OperatorType.IS: return 13; + case OperatorType.NOT: return 6; // ??? + case OperatorType.AND: return 4; + case OperatorType.OR: return 3; + case OperatorType.BETWEEN: return 7; // ??? + case OperatorType.IDIV: return 10; + case OperatorType.MOD: return 10; + case OperatorType.UNARY_PLUS: return 15; + case OperatorType.UNARY_MINUS: return 15; + case OperatorType.IS_NULL: return 15; + case OperatorType.IS_NOT_NULL: return 15; + default: return -1; + } +} + +OperatorType isOperator(string s, ref int i) { + int len = cast(int)s.length; + char ch = s[i]; + char ch2 = i < len - 1 ? s[i + 1] : 0; + //char ch3 = i < len - 2 ? s[i + 2] : 0; + if (ch == '=' && ch2 == '=') { i++; return OperatorType.EQ; } // == + if (ch == '!' && ch2 == '=') { i++; return OperatorType.NE; } // != + if (ch == '<' && ch2 == '>') { i++; return OperatorType.NE; } // <> + if (ch == '<' && ch2 == '=') { i++; return OperatorType.LE; } // <= + if (ch == '>' && ch2 == '=') { i++; return OperatorType.GE; } // >= + if (ch == '=') return OperatorType.EQ; // = + if (ch == '<') return OperatorType.LT; // < + if (ch == '>') return OperatorType.GT; // < + if (ch == '*') return OperatorType.MUL; // < + if (ch == '+') return OperatorType.ADD; // < + if (ch == '-') return OperatorType.SUB; // < + if (ch == '/') return OperatorType.DIV; // < + return OperatorType.NONE; +} + + +enum TokenType { + Keyword, // WHERE + Ident, // ident + Number, // 25 13.5e-10 + String, // 'string' + Operator, // == != <= >= < > + - * / + Dot, // . + OpenBracket, // ( + CloseBracket, // ) + Comma, // , + Entity, // entity name + Field, // field name of some entity + Alias, // alias name of some entity + Parameter, // ident after : + // types of compound AST nodes + Expression, // any expression + Braces, // ( tokens ) + CommaDelimitedList, // tokens, ... , tokens + OpExpr, // operator expression; current token == operator, children = params +} + +class Token { + int pos; + TokenType type; + KeywordType keyword = KeywordType.NONE; + OperatorType operator = OperatorType.NONE; + string text; + string spaceAfter; + EntityInfo entity; + PropertyInfo field; + FromClauseItem from; + // Embedded fields may have a prefix derived from `@Embedded` annotations on properties that + // contain them. + string columnPrefix; + Token[] children; + this(int pos, TokenType type, string text) { + this.pos = pos; + this.type = type; + this.text = text; + } + this(int pos, KeywordType keyword, string text) { + this.pos = pos; + this.type = TokenType.Keyword; + this.keyword = keyword; + this.text = text; + } + this(int pos, OperatorType op, string text) { + this.pos = pos; + this.type = TokenType.Operator; + this.operator = op; + this.text = text; + } + this(int pos, TokenType type, Token[] base, int start, int end) { + this.pos = pos; + this.type = type; + this.children = new Token[end - start]; + for (int i = start; i < end; i++) + children[i - start] = base[i]; + } + // unary operator expression + this(int pos, OperatorType type, string text, Token right) { + this.pos = pos; + this.type = TokenType.OpExpr; + this.operator = type; + this.text = text; + this.children = new Token[1]; + this.children[0] = right; + } + // binary operator expression + this(int pos, OperatorType type, string text, Token left, Token right) { + this.pos = pos; + this.type = TokenType.OpExpr; + this.text = text; + this.operator = type; + this.children = new Token[2]; + this.children[0] = left; + this.children[1] = right; + } + bool isExpression() { + return type==TokenType.Expression || type==TokenType.Braces || type==TokenType.OpExpr || type==TokenType.Parameter + || type==TokenType.Field || type==TokenType.String || type==TokenType.Number; + } + bool isCompound() { + return this.type >= TokenType.Expression; + } + string dump(int level) { + string res; + for (int i=0; i= < > + - * / + case TokenType.Dot: return "."; // . + case TokenType.OpenBracket: return "("; // ( + case TokenType.CloseBracket: return ")"; // ) + case TokenType.Comma: return ","; // , + case TokenType.Entity: return "entity: " ~ entity.name; // entity name + case TokenType.Field: return from.entityAlias ~ "." ~ field.propertyName; // field name of some entity + case TokenType.Alias: return "alias: " ~ text; // alias name of some entity + case TokenType.Parameter: return ":" ~ text; // ident after : + // types of compound AST nodes + case TokenType.Expression: return "expr"; // any expression + case TokenType.Braces: return "()"; // ( tokens ) + case TokenType.CommaDelimitedList: return ",,,"; // tokens, ... , tokens + case TokenType.OpExpr: return "" ~ text; + default: return "UNKNOWN"; + } + } + +} + +Token[] tokenize(string s) { + Token[] res; + int startpos = 0; + int state = 0; + int len = cast(int)s.length; + for (int i=0; i 0, "Invalid parameter name near " ~ cast(string)s[startpos .. $]); + res ~= new Token(startpos, TokenType.Parameter, text); + } else if (isAlpha(ch) || ch=='_' || quotedIdent) { + // identifier or keyword + if (quotedIdent) { + i++; + enforceHelper!QuerySyntaxException(i < len - 1, "Invalid quoted identifier near " ~ cast(string)s[startpos .. $]); + } + // && state == 0 + for(int j=i; j 0, "Invalid quoted identifier near " ~ cast(string)s[startpos .. $]); + if (quotedIdent) { + enforceHelper!QuerySyntaxException(i < len - 1 && s[i + 1] == '`', "Invalid quoted identifier near " ~ cast(string)s[startpos .. $]); + i++; + } + KeywordType keywordId = isKeyword(text); + if (keywordId != KeywordType.NONE && !quotedIdent) { + OperatorType keywordOp = isOperator(keywordId); + if (keywordOp != OperatorType.NONE) + res ~= new Token(startpos, keywordOp, text); // operator keyword + else + res ~= new Token(startpos, keywordId, text); + } else + res ~= new Token(startpos, TokenType.Ident, text); + } else if (isWhite(ch)) { + // whitespace + for(int j=i; j 0) { + res[$ - 1].spaceAfter = text; + } + } else if (ch == '\'') { + // string constant + i++; + for(int j=i; j= len - 1 || !isAlpha(s[i]), "Invalid number near " ~ cast(string)s[startpos .. $]); + res ~= new Token(startpos, TokenType.Number, text); + } else if (ch == '.') { + res ~= new Token(startpos, TokenType.Dot, "."); + } else if (ch == '(') { + res ~= new Token(startpos, TokenType.OpenBracket, "("); + } else if (ch == ')') { + res ~= new Token(startpos, TokenType.CloseBracket, ")"); + } else if (ch == ',') { + res ~= new Token(startpos, TokenType.Comma, ","); + } else { + enforceHelper!QuerySyntaxException(false, "Invalid character near " ~ cast(string)s[startpos .. $]); + } + } + return res; +} + +unittest { + Token[] tokens; + tokens = tokenize("SELECT a From User a where a.flags = 12 AND a.name='john' ORDER BY a.idx ASC"); + assert(tokens.length == 23); + assert(tokens[0].type == TokenType.Keyword); + assert(tokens[2].type == TokenType.Keyword); + assert(tokens[5].type == TokenType.Keyword); + assert(tokens[5].text == "where"); + assert(tokens[10].type == TokenType.Number); + assert(tokens[10].text == "12"); + assert(tokens[16].type == TokenType.String); + assert(tokens[16].text == "john"); + assert(tokens[22].type == TokenType.Keyword); + assert(tokens[22].text == "ASC"); +} + +class ParameterValues { + Variant[string] values; + int[][string]params; + int[string]unboundParams; + this(int[][string]params) { + this.params = params; + foreach(key, value; params) { + unboundParams[key] = 1; + } + } + void setParameter(string name, Variant value) { + enforceHelper!QueryParameterException((name in params) !is null, "Attempting to set unknown parameter " ~ name); + unboundParams.remove(name); + values[name] = value; + } + void checkAllParametersSet() { + if (unboundParams.length == 0) + return; + string list; + foreach(key, value; unboundParams) { + if (list.length > 0) + list ~= ", "; + list ~= key; + } + enforceHelper!QueryParameterException(false, "Parameters " ~ list ~ " not set"); + } + void applyParams(DataSetWriter ds) { + foreach(key, indexes; params) { + Variant value = values[key]; + foreach(i; indexes) + ds.setVariant(i, value); + } + } +} + +class ParsedQuery { + private string _hql; + private string _sql; + private int[][string]params; // contains 1-based indexes of ? ? ? placeholders in SQL for param by name + private int paramIndex = 1; + private FromClause _from; + private SelectClauseItem[] _select; + private EntityInfo _entity; + private int _colCount = 0; + this(string hql) { + _hql = hql; + } + @property string hql() { return _hql; } + @property string sql() { return _sql; } + @property const(EntityInfo)entity() { return _entity; } + @property int colCount() { return _colCount; } + @property FromClause from() { return _from; } + @property SelectClauseItem[] select() { return _select; } + void setEntity(const EntityInfo entity) { + _entity = cast(EntityInfo)entity; + } + void setFromClause(FromClause from) { + _from = from; + } + void setSelect(SelectClauseItem[] items) { + _select = items; + } + void setColCount(int cnt) { _colCount = cnt; } + void addParam(string paramName) { + if ((paramName in params) is null) { + params[paramName] = [paramIndex++]; + } else { + params[paramName] ~= [paramIndex++]; + } + } + int[] getParam(string paramName) { + if ((paramName in params) is null) { + throw new HibernatedException("Parameter " ~ paramName ~ " not found in query " ~ _hql); + } else { + return params[paramName]; + } + } + void appendSQL(string sql) { + _sql ~= sql; + } + void appendSpace() { + if (_sql.length > 0 && _sql[$ - 1] != ' ') + _sql ~= ' '; + } + ParameterValues createParams() { + return new ParameterValues(params); + } +} + +unittest { + ParsedQuery q = new ParsedQuery("FROM User where id = :param1 or id = :param2"); + q.addParam("param1"); // 1 + q.addParam("param2"); // 2 + q.addParam("param1"); // 3 + q.addParam("param1"); // 4 + q.addParam("param3"); // 5 + q.addParam("param2"); // 6 + assert(q.getParam("param1") == [1,3,4]); + assert(q.getParam("param2") == [2,6]); + assert(q.getParam("param3") == [5]); +} + +/+ +unittest { + + //trace("query unittest"); + import hibernated.tests; + + EntityMetaData schema = new SchemaInfoImpl!(User, Customer, AccountType, Address, Person, MoreInfo, EvenMoreInfo, Role); + 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"); + assert(parser.parameterNames.length == 2); + //trace("param1=" ~ parser.parameterNames[0]); + //trace("param2=" ~ parser.parameterNames[1]); + assert(parser.parameterNames[0] == "Id"); + assert(parser.parameterNames[1] == "skipName"); + assert(parser.fromClause.length == 1); + assert(parser.fromClause.first.entity.name == "User"); + assert(parser.fromClause.first.entityAlias == "a"); + assert(parser.selectClause.length == 1); + assert(parser.selectClause[0].prop is null); + assert(parser.selectClause[0].from.entity.name == "User"); + assert(parser.orderByClause.length == 2); + assert(parser.orderByClause[0].prop.propertyName == "name"); + assert(parser.orderByClause[0].from.entity.name == "User"); + assert(parser.orderByClause[0].asc == true); + assert(parser.orderByClause[1].prop.propertyName == "flags"); + assert(parser.orderByClause[1].from.entity.name == "User"); + assert(parser.orderByClause[1].asc == false); + + 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"); + assert(parser.whereClause !is null); + //trace(parser.whereClause.dump(0)); + Dialect dialect = new MySQLDialect(); + + assert(dialect.quoteSqlString("abc") == "'abc'"); + assert(dialect.quoteSqlString("a'b'c") == "'a\\'b\\'c'"); + assert(dialect.quoteSqlString("a\nc") == "'a\\nc'"); + + parser = new QueryParser(schema, "FROM User AS u WHERE id = :Id and u.name like '%test%'"); + ParsedQuery q = parser.makeSQL(dialect); + //trace(parser.whereClause.dump(0)); + //trace(q.hql ~ "\n=>\n" ~ q.sql); + + //trace(q.hql); + //trace(q.sql); + 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"); + assert(parser.fromClause.hasAlias("a")); + assert(parser.fromClause.hasAlias("b")); + assert(parser.fromClause.findByAlias("a").entityName == "Person"); + assert(parser.fromClause.findByAlias("b").entityName == "MoreInfo"); + assert(parser.fromClause.findByAlias("b").joinType == JoinType.LeftJoin); + assert(parser.fromClause.findByAlias("c").entityName == "EvenMoreInfo"); + // indirect JOIN + parser = new QueryParser(schema, "SELECT a FROM Person a WHERE a.id = :Id AND a.moreInfo.evenMore.flags > 0"); + assert(parser.fromClause.hasAlias("a")); + assert(parser.fromClause.length == 3); + assert(parser.fromClause[0].entity.tableName == "person"); + assert(parser.fromClause[1].entity.tableName == "person_info"); + assert(parser.fromClause[1].joinType == JoinType.InnerJoin); + assert(parser.fromClause[1].pathString == "a.moreInfo"); + assert(parser.fromClause[2].entity.tableName == "person_info2"); + assert(parser.fromClause[2].joinType == JoinType.LeftJoin); + assert(parser.fromClause[2].pathString == "a.moreInfo.evenMore"); + // indirect JOIN, no alias + parser = new QueryParser(schema, "FROM Person WHERE id = :Id AND moreInfo.evenMore.flags > 0"); + assert(parser.fromClause.length == 3); + assert(parser.fromClause[0].entity.tableName == "person"); + assert(parser.fromClause[0].fetch == true); + //trace("select fields [" ~ to!string(parser.fromClause[0].startColumn) ~ ", " ~ to!string(parser.fromClause[0].selectedColumns) ~ "]"); + //trace("select fields [" ~ to!string(parser.fromClause[1].startColumn) ~ ", " ~ to!string(parser.fromClause[1].selectedColumns) ~ "]"); + //trace("select fields [" ~ to!string(parser.fromClause[2].startColumn) ~ ", " ~ to!string(parser.fromClause[2].selectedColumns) ~ "]"); + assert(parser.fromClause[0].selectedColumns == 4); + assert(parser.fromClause[1].entity.tableName == "person_info"); + assert(parser.fromClause[1].joinType == JoinType.InnerJoin); + assert(parser.fromClause[1].pathString == "_a1.moreInfo"); + assert(parser.fromClause[1].fetch == true); + assert(parser.fromClause[1].selectedColumns == 2); + assert(parser.fromClause[2].entity.tableName == "person_info2"); + assert(parser.fromClause[2].joinType == JoinType.LeftJoin); + assert(parser.fromClause[2].pathString == "_a1.moreInfo.evenMore"); + assert(parser.fromClause[2].fetch == true); + assert(parser.fromClause[2].selectedColumns == 3); + + q = parser.makeSQL(dialect); + //trace(q.hql); + //trace(q.sql); + + parser = new QueryParser(schema, "FROM User WHERE id in (1, 2, (3 - 1 * 25) / 2, 4 + :Id, 5)"); + //trace(parser.whereClause.dump(0)); + q = parser.makeSQL(dialect); + //trace(q.hql); + //trace(q.sql); + + parser = new QueryParser(schema, "FROM Customer WHERE users.id = 1"); + q = parser.makeSQL(dialect); +// trace(q.hql); +// trace(q.sql); + 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"); + + parser = new QueryParser(schema, "FROM Customer WHERE id = 1"); + q = parser.makeSQL(dialect); +// trace(q.hql); +// trace(q.sql); + 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"); + + parser = new QueryParser(schema, "FROM User WHERE roles.id = 1"); + q = parser.makeSQL(dialect); + //trace(q.hql); + //trace(q.sql); + 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"); + + parser = new QueryParser(schema, "FROM Role WHERE users.id = 1"); + q = parser.makeSQL(dialect); +// trace(q.hql); +// trace(q.sql); + 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"); + + parser = new QueryParser(schema, "FROM User WHERE customer.id = 1"); + q = parser.makeSQL(dialect); +// trace(q.hql); +// trace(q.sql); + 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"); + + parser = new QueryParser(schema, "SELECT a2 FROM User AS a1 JOIN a1.roles AS a2 WHERE a1.id = 1"); + q = parser.makeSQL(dialect); + //trace(q.hql); + //trace(q.sql); + 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"); + + parser = new QueryParser(schema, "SELECT a2 FROM Customer AS a1 JOIN a1.users AS a2 WHERE a1.id = 1"); + q = parser.makeSQL(dialect); + //trace(q.hql); + //trace(q.sql); + 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"); + +} ++/ diff --git a/source/hibernated/tests.d b/source/hibernated/tests.d index 55172aa..62bd110 100644 --- a/source/hibernated/tests.d +++ b/source/hibernated/tests.d @@ -1,13 +1,13 @@ /** - * HibernateD - Object-Relation Mapping for D programming language, with interface similar to Hibernate. - * + * HibernateD - Object-Relation Mapping for D programming language, with interface similar to Hibernate. + * * Hibernate documentation can be found here: * $(LINK http://hibernate.org/docs)$(BR) - * + * * Source file hibernated/tests.d. * * This module contains unit tests for functional testing on real DB. - * + * * Copyright: Copyright 2013 * License: $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0). * Author: Vadim Lopatin @@ -26,17 +26,18 @@ private import ddbc.core : Connection, DataSource, Statement; private import hibernated.core; +/+ version(unittest) { //@Entity @Table("users") // to override table name - "users" instead of default "user" class User { - + //@Generated long id; - + string name; - + // property column private long _flags; @Null // override NotNull which is inferred from long type @@ -50,21 +51,21 @@ version(unittest) { @Column(null, 1024) // override default length, autogenerate column name) string getComment() { return comment; } void setComment(string v) { comment = v; } - + //@ManyToOne -- not mandatory, will be deduced //@JoinColumn("customer_fk") Customer customer; - + @ManyToMany LazyCollection!Role roles; - + override string toString() { return "id=" ~ to!string(id) ~ ", name=" ~ name ~ ", flags=" ~ to!string(flags) ~ ", comment=" ~ comment ~ ", customerId=" ~ (customer is null ? "NULL" : customer.toString()); } - + } - - + + //@Entity @Table("customers") // to override table name - "customers" instead of default "customer" class Customer { @@ -75,19 +76,19 @@ version(unittest) { // deduced as @Embedded automatically Address address; - + //@ManyToOne -- not mandatory, will be deduced //@JoinColumn("account_type_fk") Lazy!AccountType accountType; - + // @OneToMany("customer") // LazyCollection!User users; - + //@OneToMany("customer") -- not mandatory, will be deduced private User[] _users; @property User[] users() { return _users; } @property void users(User[] value) { _users = value; } - + this() { address = new Address(); } @@ -97,7 +98,7 @@ version(unittest) { } static assert(isEmbeddedObjectMember!(Customer, "address")); - + @Embeddable class Address { hibernated.type.String zip; @@ -110,53 +111,53 @@ version(unittest) { return " zip=" ~ zip ~ ", city=" ~ city ~ ", streetAddress=" ~ streetAddress; } } - + @Entity // need to have at least one annotation to import automatically from module class AccountType { //@Generated int id; string name; } - - //@Entity + + //@Entity class Role { //@Generated int id; string name; - @ManyToMany + @ManyToMany LazyCollection!User users; } - + //@Entity //@Table("t1") class T1 { - //@Id + //@Id //@Generated int id; - - //@NotNull + + //@NotNull @UniqueKey string name; - + // property column private long _flags; // @Column -- not mandatory, will be deduced @property long flags() { return _flags; } @property void flags(long v) { _flags = v; } - + // getter/setter property private string comment; // @Column -- not mandatory, will be deduced @Null string getComment() { return comment; } void setComment(string v) { comment = v; } - - + + override string toString() { return "id=" ~ to!string(id) ~ ", name=" ~ name ~ ", flags=" ~ to!string(flags) ~ ", comment=" ~ comment; } } - + @Entity static class GeneratorTest { //@Generator("std.uuid.randomUUID().toString()") @@ -164,7 +165,7 @@ version(unittest) { string id; string name; } - + @Entity static class TypeTest { //@Generated @@ -198,7 +199,7 @@ version(unittest) { byte[] byte_array_field; ubyte[] ubyte_array_field; } - + import ddbc.drivers.mysqlddbc; import ddbc.drivers.pgsqlddbc; import ddbc.drivers.sqliteddbc; @@ -207,8 +208,8 @@ version(unittest) { import hibernated.dialects.sqlitedialect; import hibernated.dialects.pgsqldialect; - - string[] UNIT_TEST_DROP_TABLES_SCRIPT = + + string[] UNIT_TEST_DROP_TABLES_SCRIPT = [ "DROP TABLE IF EXISTS role_users", "DROP TABLE IF EXISTS account_type", @@ -220,7 +221,7 @@ version(unittest) { "DROP TABLE IF EXISTS role", "DROP TABLE IF EXISTS generator_test", ]; - string[] UNIT_TEST_CREATE_TABLES_SCRIPT = + string[] UNIT_TEST_CREATE_TABLES_SCRIPT = [ "CREATE TABLE IF NOT EXISTS role (id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL)", "CREATE TABLE IF NOT EXISTS account_type (id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL)", @@ -232,7 +233,7 @@ version(unittest) { "CREATE TABLE IF NOT EXISTS role_users (role_fk int not null, user_fk int not null, primary key (role_fk, user_fk), unique index(user_fk, role_fk))", "CREATE TABLE IF NOT EXISTS generator_test (id varchar(64) not null primary key, name varchar(255) not null)", ]; - string[] UNIT_TEST_FILL_TABLES_SCRIPT = + string[] UNIT_TEST_FILL_TABLES_SCRIPT = [ "INSERT INTO role (name) VALUES ('admin')", "INSERT INTO role (name) VALUES ('viewer')", @@ -322,10 +323,10 @@ version(unittest) { unittest { - + // Checking generated metadata EntityMetaData schema = new SchemaInfoImpl!(User, Customer, AccountType, T1, TypeTest, Address, Role); - + //writeln("metadata test 1"); assert(schema["TypeTest"].length==29); assert(schema.getEntityCount() == 7); @@ -426,15 +427,15 @@ unittest { // schema.setPropertyValue(e2user, "customer", Variant(c42)); // assert(e2user.customer.id == 42); // //assert(schema.getPropertyValue(e2user, "customer") == 42); - + Object e1 = schema.findEntity("User").createEntity(); assert(e1 !is null); User e1user = cast(User)e1; assert(e1user !is null); e1user.id = 25; - - - + + + } unittest { @@ -443,7 +444,7 @@ unittest { //writeln("metadata test 2"); - + // Checking generated metadata EntityMetaData schema = new SchemaInfoImpl!(User, Customer, AccountType, T1, TypeTest, Address, Role, GeneratorTest, Person, MoreInfo, EvenMoreInfo); Dialect dialect = getUnitTestDialect(); @@ -495,7 +496,7 @@ unittest { { Session sess = factory.openSession(); scope(exit) sess.close(); - + User u1 = sess.load!User(1); //writeln("Loaded value: " ~ u1.toString); assert(u1.id == 1); @@ -505,29 +506,29 @@ unittest { assert(u1.customer.accountType().name == "Type1"); Role[] u1roles = u1.roles; assert(u1roles.length == 2); - + User u2 = sess.load!User(2); assert(u2.name == "user 2"); assert(u2.flags == 22); // NULL is loaded as 0 if property cannot hold nulls - + User u3 = sess.get!User(3); assert(u3.name == "user 3"); assert(u3.flags == 0); // NULL is loaded as 0 if property cannot hold nulls assert(u3.getComment() !is null); assert(u3.customer.name == "customer 2"); assert(u3.customer.accountType() is null); - + User u4 = new User(); sess.load(u4, 4); assert(u4.name == "user 4"); assert(u4.getComment() is null); - + User u5 = new User(); u5.id = 5; sess.refresh(u5); assert(u5.name == "test user 5"); //assert(u5.customer !is null); - + u5 = sess.load!User(5); assert(u5.name == "test user 5"); assert(u5.customer !is null); @@ -535,12 +536,12 @@ unittest { assert(u5.customer.name == "customer 3"); assert(u5.customer.accountType() !is null); assert(u5.customer.accountType().name == "Type2"); - + User u6 = sess.load!User(6); assert(u6.name == "test user 6"); assert(u6.customer is null); - - // + + // //writeln("loading customer 3"); // testing @Embedded property Customer c3 = sess.load!Customer(3); @@ -548,13 +549,13 @@ unittest { assert(c3.address.streetAddress == "Baker Street, 24"); c3.address.streetAddress = "Baker Street, 24/2"; c3.address.zip = "55555"; - + User[] c3users = c3.users; //writeln(" *** customer has " ~ to!string(c3users.length) ~ " users"); assert(c3users.length == 2); assert(c3users[0].customer == c3); assert(c3users[1].customer == c3); - + //writeln("updating customer 3"); sess.update(c3); Customer c3_reloaded = sess.load!Customer(3); @@ -566,37 +567,37 @@ unittest { Session sess = factory.openSession(); scope(exit) sess.close(); - + // check Session.save() when id is filled Customer c4 = new Customer(); c4.id = 4; c4.name = "Customer_4"; sess.save(c4); - + Customer c4_check = sess.load!Customer(4); assert(c4.id == c4_check.id); assert(c4.name == c4_check.name); - + sess.remove(c4); - + c4 = sess.get!Customer(4); assert (c4 is null); - + Customer c5 = new Customer(); c5.name = "Customer_5"; sess.save(c5); - + // Testing generator function (uuid) GeneratorTest g1 = new GeneratorTest(); g1.name = "row 1"; assert(g1.id is null); sess.save(g1); assert(g1.id !is null); - - + + assertThrown!MappingException(sess.createQuery("SELECT id, name, blabla FROM User ORDER BY name")); assertThrown!QuerySyntaxException(sess.createQuery("SELECT id: name FROM User ORDER BY name")); - + // test multiple row query Query q = sess.createQuery("FROM User ORDER BY name"); User[] list = q.list!User(); @@ -615,7 +616,7 @@ unittest { // } assertThrown!HibernatedException(q.uniqueResult!User()); assertThrown!HibernatedException(q.uniqueRow()); - + } { Session sess = factory.openSession(); @@ -637,14 +638,14 @@ unittest { Variant[] row = q.uniqueRow(); assert(row[0] == 6L); assert(row[1] == "test user 6"); - + // test empty SELECT result q.setParameter("Id", Variant(7)); row = q.uniqueRow(); assert(row is null); uu = q.uniqueResult!User(); assert(uu is null); - + q = sess.createQuery("SELECT c.name, c.address.zip FROM Customer AS c WHERE id = :Id").setParameter("Id", Variant(1)); row = q.uniqueRow(); assert(row !is null); @@ -717,65 +718,65 @@ unittest { version (unittest) { // for testing of Embeddable - @Embeddable + @Embeddable class EMName { string firstName; string lastName; } - - //@Entity + + //@Entity class EMUser { //@Id @Generated //@Column int id; - + // deduced as @Embedded automatically EMName userName; } - + // for testing of Embeddable //@Entity class Person { //@Id int id; - - // @Column @NotNull + + // @Column @NotNull string firstName; - // @Column @NotNull + // @Column @NotNull string lastName; - + @NotNull @OneToOne @JoinColumn("more_info_fk") MoreInfo moreInfo; } - - + + //@Entity @Table("person_info") class MoreInfo { //@Id @Generated int id; - // @Column + // @Column long flags; @OneToOne("moreInfo") Person person; @OneToOne("personInfo") EvenMoreInfo evenMore; } - + //@Entity @Table("person_info2") class EvenMoreInfo { //@Id @Generated int id; - //@Column + //@Column long flags; @OneToOne @JoinColumn("person_info_fk") MoreInfo personInfo; } - + } @@ -786,10 +787,10 @@ unittest { static assert(getPropertyEmbeddedEntityName!(EMUser, "userName") == "EMName"); static assert(getPropertyEmbeddedClassName!(EMUser, "userName") == "hibernated.tests.EMName"); //pragma(msg, getEmbeddedPropertyDef!(EMUser, "userName")()); - + // Checking generated metadata EntityMetaData schema = new SchemaInfoImpl!(EMName, EMUser); - + static assert(hasMemberAnnotation!(Person, "moreInfo", OneToOne)); static assert(getPropertyReferencedEntityName!(Person, "moreInfo") == "MoreInfo"); static assert(getPropertyReferencedClassName!(Person, "moreInfo") == "hibernated.tests.MoreInfo"); @@ -800,24 +801,24 @@ unittest { static assert(getJoinColumnName!(Person, "moreInfo") == "more_info_fk"); static assert(getOneToOneReferencedPropertyName!(MoreInfo, "person") == "moreInfo"); static assert(getOneToOneReferencedPropertyName!(Person, "moreInfo") is null); - + //pragma(msg, "done getOneToOneReferencedPropertyName"); - + // Checking generated metadata //EntityMetaData schema = new SchemaInfoImpl!(Person, MoreInfo); // foreach(e; schema["Person"]) { // writeln("property: " ~ e.propertyName); // } - schema = new SchemaInfoImpl!(hibernated.tests); //Person, MoreInfo, EvenMoreInfo, - + schema = new SchemaInfoImpl!(hibernated.tests); //Person, MoreInfo, EvenMoreInfo, + { - + int[Variant] map0; map0[Variant(1)] = 3; assert(map0[Variant(1)] == 3); map0[Variant(1)]++; assert(map0[Variant(1)] == 4); - + //writeln("map test"); PropertyLoadMap map = new PropertyLoadMap(); Person ppp1 = new Person(); @@ -853,13 +854,13 @@ unittest { assert(m[Variant(1)].length == 1); assert(m[Variant(2)].length == 2); } - + if (DB_TESTS_ENABLED) { //recreateTestSchema(); - + //writeln("metadata test 2"); import hibernated.dialects.mysqldialect; - + // Checking generated metadata Dialect dialect = getUnitTestDialect(); DataSource ds = getUnitTestDataSource(); @@ -873,7 +874,7 @@ unittest { auto p1 = sess.get!Person(1); assert(p1.firstName == "Andrei"); - + // all non-null oneToOne relations auto q = sess.createQuery("FROM Person WHERE id=:Id").setParameter("Id", Variant(1)); @@ -900,8 +901,7 @@ unittest { assert(p2.moreInfo.evenMore is null); } - + } } - - ++/