/* * DynamORM - Dynamic Object-Relational Mapping library. * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com) * All rights reserved. * * Some of methods in this code file is based on Kerosene ORM solution * for parsing dynamic lambda expressions by Moisés Barba Cebeira * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Text; using DynamORM.Builders.Extensions; using DynamORM.Helpers; using DynamORM.Helpers.Dynamics; using DynamORM.Mapper; namespace DynamORM.Builders.Implementation { /// Implementation of dynamic select query builder. internal class DynamicSelectQueryBuilder : DynamicQueryBuilder, IDynamicSelectQueryBuilder, DynamicQueryBuilder.IQueryWithWhere, DynamicQueryBuilder.IQueryWithHaving { private int? _limit = null; private int? _offset = null; private bool _distinct = false; private string _select; private string _from; protected string _join; private string _groupby; private string _orderby; #region IQueryWithHaving /// Gets or sets the having condition. public string HavingCondition { get; set; } /// Gets or sets the amount of not closed brackets in having statement. public int HavingOpenBracketsCount { get; set; } #endregion IQueryWithHaving /// /// Gets a value indicating whether this instance has select columns. /// public bool HasSelectColumns { get { return !string.IsNullOrEmpty(_select); } } /// /// Initializes a new instance of the class. /// /// The database. public DynamicSelectQueryBuilder(DynamicDatabase db) : base(db) { } /// /// Initializes a new instance of the class. /// /// The database. /// The parent query. internal DynamicSelectQueryBuilder(DynamicDatabase db, DynamicQueryBuilder parent) : base(db, parent) { } /// Generates the text this command will execute against the underlying database. /// The text to execute against the underlying database. public override string CommandText() { bool lused = false; bool oused = false; StringBuilder sb = new StringBuilder("SELECT"); if (_distinct) sb.AppendFormat(" DISTINCT"); if (_limit.HasValue) { if ((Database.Options & DynamicDatabaseOptions.SupportTop) == DynamicDatabaseOptions.SupportTop) { sb.AppendFormat(" TOP {0}", _limit); lused = true; } else if ((Database.Options & DynamicDatabaseOptions.SupportFirstSkip) == DynamicDatabaseOptions.SupportFirstSkip) { sb.AppendFormat(" FIRST {0}", _limit); lused = true; } } if (_offset.HasValue && (Database.Options & DynamicDatabaseOptions.SupportFirstSkip) == DynamicDatabaseOptions.SupportFirstSkip) { sb.AppendFormat(" SKIP {0}", _offset); oused = true; } if (_select != null) sb.AppendFormat(" {0}", _select); else sb.Append(" *"); if (_from != null) sb.AppendFormat(" FROM {0}", _from); if (_join != null) sb.AppendFormat(" {0}", _join); if (WhereCondition != null) sb.AppendFormat(" WHERE {0}", WhereCondition); if (_groupby != null) sb.AppendFormat(" GROUP BY {0}", _groupby); if (HavingCondition != null) sb.AppendFormat(" HAVING {0}", HavingCondition); if (_orderby != null) sb.AppendFormat(" ORDER BY {0}", _orderby); if (_limit.HasValue && !lused && (Database.Options & DynamicDatabaseOptions.SupportLimitOffset) == DynamicDatabaseOptions.SupportLimitOffset) sb.AppendFormat(" LIMIT {0}", _limit); if (_offset.HasValue && !oused && (Database.Options & DynamicDatabaseOptions.SupportLimitOffset) == DynamicDatabaseOptions.SupportLimitOffset) sb.AppendFormat(" OFFSET {0}", _offset); return sb.ToString(); } #region Execution /// Execute this builder. /// Enumerator of objects expanded from query. public virtual IEnumerable Execute() { using (IDbConnection con = Database.Open()) using (IDbCommand cmd = con.CreateCommand()) { using (IDataReader rdr = cmd .SetCommand(this) .ExecuteReader()) using (IDataReader cache = new DynamicCachedReader(rdr)) while (cache.Read()) { dynamic val = null; // Work around to avoid yield being in try...catchblock: // http://stackoverflow.com/questions/346365/why-cant-yield-return-appear-inside-a-try-block-with-a-catch try { val = cache.RowToDynamic(); } catch (ArgumentException argex) { StringBuilder sb = new StringBuilder(); cmd.Dump(sb); throw new ArgumentException( string.Format("{0}{1}{2}", argex.Message, Environment.NewLine, sb), argex.InnerException.NullOr(a => a, argex)); } yield return val; } } } /// Execute this builder and map to given type. /// Type of object to map on. /// Enumerator of objects expanded from query. public virtual IEnumerable Execute() where T : class { DynamicTypeMap mapper = DynamicMapperCache.GetMapper(); if (mapper == null) throw new InvalidOperationException("Type can't be mapped for unknown reason."); using (IDbConnection con = Database.Open()) using (IDbCommand cmd = con.CreateCommand()) { using (IDataReader rdr = cmd .SetCommand(this) .ExecuteReader()) using (IDataReader cache = new DynamicCachedReader(rdr)) while (cache.Read()) { dynamic val = null; // Work around to avoid yield being in try...catchblock: // http://stackoverflow.com/questions/346365/why-cant-yield-return-appear-inside-a-try-block-with-a-catch try { val = cache.RowToDynamic(); } catch (ArgumentException argex) { StringBuilder sb = new StringBuilder(); cmd.Dump(sb); throw new ArgumentException( string.Format("{0}{1}{2}", argex.Message, Environment.NewLine, sb), argex.InnerException.NullOr(a => a, argex)); } yield return mapper.Create(val) as T; } } } /// Execute this builder as a data reader. /// Action containing reader. public virtual void ExecuteDataReader(Action reader) { using (IDbConnection con = Database.Open()) using (IDbCommand cmd = con.CreateCommand()) using (IDataReader rdr = cmd .SetCommand(this) .ExecuteReader()) reader(rdr); } /// Execute this builder as a data reader, but /// first makes a full reader copy in memory. /// Action containing reader. public virtual void ExecuteCachedDataReader(Action reader) { using (IDbConnection con = Database.Open()) using (IDbCommand cmd = con.CreateCommand()) using (IDataReader rdr = cmd .SetCommand(this) .ExecuteReader()) using (IDataReader cache = new DynamicCachedReader(rdr)) reader(cache); } /// Returns a single result. /// Result of a query. public virtual object Scalar() { using (IDbConnection con = Database.Open()) using (IDbCommand cmd = con.CreateCommand()) { return cmd .SetCommand(this) .ExecuteScalar(); } } #if !DYNAMORM_OMMIT_GENERICEXECUTION && !DYNAMORM_OMMIT_TRYPARSE /// Returns a single result. /// Type to parse to. /// Default value. /// Result of a query. public virtual T ScalarAs(T defaultValue = default(T)) { using (IDbConnection con = Database.Open()) using (IDbCommand cmd = con.CreateCommand()) { return cmd .SetCommand(this) .ExecuteScalarAs(defaultValue); } } #endif #endregion Execution #region From/Join /// /// Adds to the 'From' clause the contents obtained by parsing the dynamic lambda expressions given. The supported /// formats are: /// - Resolve to a string: 'x => "Table AS Alias', where the alias part is optional. /// - Resolve to an expression: 'x => x.Table.As( x.Alias )', where the alias part is optional. /// - Generic expression: 'x => x( expression ).As( x.Alias )', where the alias part is mandatory. In this /// case the alias is not annotated. /// /// The specification. /// The specification. /// This instance to permit chaining. public virtual IDynamicSelectQueryBuilder From(Func fn, params Func[] func) { if (fn == null) throw new ArgumentNullException("Array of functions cannot be or contain null."); int index = FromFunc(-1, fn); foreach (Func f in func) index = FromFunc(index, f); return this; } private int FromFunc(int index, Func f) { if (f == null) throw new ArgumentNullException("Array of functions cannot be or contain null."); index++; ITableInfo tableInfo = null; using (DynamicParser parser = DynamicParser.Parse(f)) { object result = parser.Result; // If the expression result is string. if (result is string) { string node = (string)result; Tuple tuple = node.SplitSomethingAndAlias(); string[] parts = tuple.Item1.Split('.'); tableInfo = new TableInfo(Database, Database.StripName(parts.Last()).Validated("Table"), tuple.Item2.Validated("Alias", canbeNull: true), parts.Length == 2 ? Database.StripName(parts.First()).Validated("Owner", canbeNull: true) : null); } else if (result is Type) { Type type = (Type)result; if (type.IsAnonymous()) throw new InvalidOperationException(string.Format("Cant assign anonymous type as a table ({0}). Parsing {1}", type.FullName, result)); DynamicTypeMap mapper = DynamicMapperCache.GetMapper(type); if (mapper == null) throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}). Parsing {1}", type.FullName, result)); tableInfo = new TableInfo(Database, type); } else if (result is DynamicParser.Node) { // Or if it resolves to a dynamic node DynamicParser.Node node = (DynamicParser.Node)result; string owner = null; string main = null; string alias = null; bool nolock = false; Type type = null; while (true) { // Support for the AS() virtual method... if (node is DynamicParser.Node.Method && ((DynamicParser.Node.Method)node).Name.ToUpper() == "AS") { if (alias != null) throw new ArgumentException(string.Format("Alias '{0}' is already set when parsing '{1}'.", alias, result)); object[] args = ((DynamicParser.Node.Method)node).Arguments; if (args == null) throw new ArgumentNullException("arg", "AS() is not a parameterless method."); if (args.Length != 1) throw new ArgumentException("AS() requires one and only one parameter: " + args.Sketch()); alias = Parse(args[0], rawstr: true, decorate: false).Validated("Alias"); node = node.Host; continue; } // Support for the NoLock() virtual method... if (node is DynamicParser.Node.Method && ((DynamicParser.Node.Method)node).Name.ToUpper() == "NOLOCK") { object[] args = ((DynamicParser.Node.Method)node).Arguments; if (args != null && args.Length > 0) throw new ArgumentNullException("arg", "NoLock() doesn't support arguments."); nolock = true; node = node.Host; continue; } /*if (node is DynamicParser.Node.Method && ((DynamicParser.Node.Method)node).Name.ToUpper() == "subquery") { main = Parse(this.SubQuery(((DynamicParser.Node.Method)node).Arguments.Where(p => p is Func).Cast>().ToArray()), Parameters); continue; }*/ // Support for table specifications... if (node is DynamicParser.Node.GetMember) { if (owner != null) throw new ArgumentException(string.Format("Owner '{0}.{1}' is already set when parsing '{2}'.", owner, main, result)); if (main != null) owner = ((DynamicParser.Node.GetMember)node).Name; else main = ((DynamicParser.Node.GetMember)node).Name; node = node.Host; continue; } // Support for generic sources... if (node is DynamicParser.Node.Invoke) { if (owner != null) throw new ArgumentException(string.Format("Owner '{0}.{1}' is already set when parsing '{2}'.", owner, main, result)); if (main != null) owner = string.Format("{0}", Parse(node, rawstr: true, pars: Parameters)); else { DynamicParser.Node.Invoke invoke = (DynamicParser.Node.Invoke)node; if (invoke.Arguments.Length == 1 && invoke.Arguments[0] is Type) { type = (Type)invoke.Arguments[0]; if (type.IsAnonymous()) throw new InvalidOperationException(string.Format("Cant assign anonymous type as a table ({0}). Parsing {1}", type.FullName, result)); DynamicTypeMap mapper = DynamicMapperCache.GetMapper(type); if (mapper == null) throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}). Parsing {1}", type.FullName, result)); main = mapper.Table == null || string.IsNullOrEmpty(mapper.Table.Name) ? mapper.Type.Name : mapper.Table.Name; owner = (mapper.Table != null) ? mapper.Table.Owner : owner; } else main = string.Format("{0}", Parse(node, rawstr: true, pars: Parameters)); } node = node.Host; continue; } // Just finished the parsing... if (node is DynamicParser.Node.Argument) break; // All others are assumed to be part of the main element... if (main != null) main = Parse(node, pars: Parameters); else main = Parse(node, pars: Parameters); break; } if (!string.IsNullOrEmpty(main)) tableInfo = type == null ? new TableInfo(Database, main, alias, owner, nolock) : new TableInfo(Database, type, alias, owner, nolock); else throw new ArgumentException(string.Format("Specification #{0} is invalid: {1}", index, result)); } // Or it is a not supported expression... if (tableInfo == null) throw new ArgumentException(string.Format("Specification #{0} is invalid: {1}", index, result)); Tables.Add(tableInfo); // We finally add the contents... StringBuilder sb = new StringBuilder(); if (!string.IsNullOrEmpty(tableInfo.Owner)) sb.AppendFormat("{0}.", Database.DecorateName(tableInfo.Owner)); sb.Append(tableInfo.Name.ContainsAny(StringExtensions.InvalidMemberChars) ? tableInfo.Name : Database.DecorateName(tableInfo.Name)); if (!string.IsNullOrEmpty(tableInfo.Alias)) sb.AppendFormat(" AS {0}", tableInfo.Alias); if (SupportNoLock && tableInfo.NoLock) sb.AppendFormat(" WITH(NOLOCK)"); _from = string.IsNullOrEmpty(_from) ? sb.ToString() : string.Format("{0}, {1}", _from, sb.ToString()); } return index; } /// /// Adds to the 'Join' clause the contents obtained by parsing the dynamic lambda expressions given. The supported /// formats are: /// - Resolve to a string: 'x => "Table AS Alias ON Condition', where the alias part is optional. /// - Resolve to an expression: 'x => x.Table.As( x.Alias ).On( condition )', where the alias part is optional. /// - Generic expression: 'x => x( expression ).As( x.Alias ).On( condition )', where the alias part is mandatory. /// In this case the alias is not annotated. /// The expression might be prepended by a method that, in this case, is used to specify the specific join type you /// want to perform, as in: 'x => x.Left()...". Two considerations apply: /// - If a 'false' argument is used when no 'Join' part appears in its name, then no 'Join' suffix is added /// with a space in between. /// - If a 'false' argument is used when a 'Join' part does appear, then no split is performed to separate the /// 'Join' part. /// /// The specification. /// This instance to permit chaining. public virtual IDynamicSelectQueryBuilder Join(params Func[] func) { // We need to do two passes to add aliases first. return JoinInternal(true, func).JoinInternal(false, func); } /// /// Adds to the 'Join' clause the contents obtained by parsing the dynamic lambda expressions given. The supported /// formats are: /// - Resolve to a string: 'x => "Table AS Alias ON Condition', where the alias part is optional. /// - Resolve to an expression: 'x => x.Table.As( x.Alias ).On( condition )', where the alias part is optional. /// - Generic expression: 'x => x( expression ).As( x.Alias ).On( condition )', where the alias part is mandatory. /// In this case the alias is not annotated. /// The expression might be prepended by a method that, in this case, is used to specify the specific join type you /// want to perform, as in: 'x => x.Left()...". Two considerations apply: /// - If a 'false' argument is used when no 'Join' part appears in its name, then no 'Join' suffix is added /// with a space in between. /// - If a 'false' argument is used when a 'Join' part does appear, then no split is performed to separate the /// 'Join' part. /// /// If true just pass by to locate tables and aliases, otherwise create rules. /// The specification. /// This instance to permit chaining. protected virtual DynamicSelectQueryBuilder JoinInternal(bool justAddTables, params Func[] func) { if (func == null) throw new ArgumentNullException("Array of functions cannot be null."); int index = -1; foreach (Func f in func) { index++; ITableInfo tableInfo = null; if (f == null) throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); using (DynamicParser parser = DynamicParser.Parse(f)) { object result = parser.Result; if (result == null) throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); string type = null; string main = null; string owner = null; string alias = null; string condition = null; bool nolock = false; Type tableType = null; // If the expression resolves to a string... if (result is string) { string node = (string)result; int n = node.ToUpper().IndexOf("JOIN "); if (n < 0) main = node; else { // For strings we only accept 'JOIN' as a suffix type = node.Substring(0, n + 4); main = node.Substring(n + 4); } n = main.ToUpper().IndexOf("ON"); if (n >= 0) { condition = main.Substring(n + 3); main = main.Substring(0, n).Trim(); } Tuple tuple = main.SplitSomethingAndAlias(); // In this case we split on the remaining 'main' string[] parts = tuple.Item1.Split('.'); main = Database.StripName(parts.Last()).Validated("Table"); owner = parts.Length == 2 ? Database.StripName(parts.First()).Validated("Owner", canbeNull: true) : null; alias = tuple.Item2.Validated("Alias", canbeNull: true); } else if (result is DynamicParser.Node) { // Or if it resolves to a dynamic node... DynamicParser.Node node = (DynamicParser.Node)result; while (true) { // Support for the ON() virtual method... if (node is DynamicParser.Node.Method && ((DynamicParser.Node.Method)node).Name.ToUpper() == "ON") { if (condition != null) throw new ArgumentException(string.Format("Condition '{0}' is already set when parsing '{1}'.", alias, result)); object[] args = ((DynamicParser.Node.Method)node).Arguments; if (args == null) throw new ArgumentNullException("arg", "ON() is not a parameterless method."); if (args.Length != 1) throw new ArgumentException("ON() requires one and only one parameter: " + args.Sketch()); condition = Parse(args[0], rawstr: true, pars: justAddTables ? null : Parameters); node = node.Host; continue; } // Support for the AS() virtual method... if (node is DynamicParser.Node.Method && ((DynamicParser.Node.Method)node).Name.ToUpper() == "AS") { if (alias != null) throw new ArgumentException(string.Format("Alias '{0}' is already set when parsing '{1}'.", alias, result)); object[] args = ((DynamicParser.Node.Method)node).Arguments; if (args == null) throw new ArgumentNullException("arg", "AS() is not a parameterless method."); if (args.Length != 1) throw new ArgumentException("AS() requires one and only one parameter: " + args.Sketch()); alias = Parse(args[0], rawstr: true, decorate: false, isMultiPart: false).Validated("Alias"); node = node.Host; continue; } // Support for the NoLock() virtual method... if (node is DynamicParser.Node.Method && ((DynamicParser.Node.Method)node).Name.ToUpper() == "NOLOCK") { object[] args = ((DynamicParser.Node.Method)node).Arguments; if (args != null && args.Length > 0) throw new ArgumentNullException("arg", "NoLock() doesn't support arguments."); nolock = true; node = node.Host; continue; } // Support for table specifications... if (node is DynamicParser.Node.GetMember) { if (owner != null) throw new ArgumentException(string.Format("Owner '{0}.{1}' is already set when parsing '{2}'.", owner, main, result)); if (main != null) owner = ((DynamicParser.Node.GetMember)node).Name; else main = ((DynamicParser.Node.GetMember)node).Name; node = node.Host; continue; } // Support for Join Type specifications... if (node is DynamicParser.Node.Method && (node.Host is DynamicParser.Node.Argument || node.Host is DynamicParser.Node.Invoke)) { if (type != null) throw new ArgumentException(string.Format("Join type '{0}' is already set when parsing '{1}'.", main, result)); type = ((DynamicParser.Node.Method)node).Name; bool avoid = false; object[] args = ((DynamicParser.Node.Method)node).Arguments; if (args != null && args.Length > 0) { avoid = args[0] is bool && !((bool)args[0]); string proposedType = args.FirstOrDefault(a => a is string) as string; if (!string.IsNullOrEmpty(proposedType)) type = proposedType; } type = type.ToUpper(); // Normalizing, and stepping out the trivial case... if (type != "JOIN") { // Special cases // x => x.LeftOuter() / x => x.RightOuter()... type = type.Replace("OUTER", " OUTER ") .Replace(" ", " ") .Trim(' '); // x => x.Left()... int n = type.IndexOf("JOIN"); if (n < 0 && !avoid) type += " JOIN"; // x => x.InnerJoin() / x => x.JoinLeft() ... else { if (!avoid) { if (n == 0) type = type.Replace("JOIN", "JOIN "); else type = type.Replace("JOIN", " JOIN"); } } } node = node.Host; continue; } // Support for generic sources... if (node is DynamicParser.Node.Invoke) { if (owner != null) throw new ArgumentException(string.Format("Owner '{0}.{1}' is already set when parsing '{2}'.", owner, main, result)); if (main != null) owner = string.Format("{0}", Parse(node, rawstr: true, pars: justAddTables ? null : Parameters)); else { DynamicParser.Node.Invoke invoke = (DynamicParser.Node.Invoke)node; if (invoke.Arguments.Length == 1 && invoke.Arguments[0] is Type) { tableType = (Type)invoke.Arguments[0]; DynamicTypeMap mapper = DynamicMapperCache.GetMapper(tableType); if (mapper == null) throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}).", tableType.FullName)); main = mapper.Table == null || string.IsNullOrEmpty(mapper.Table.Name) ? mapper.Type.Name : mapper.Table.Name; owner = (mapper.Table != null) ? mapper.Table.Owner : owner; } else main = string.Format("{0}", Parse(node, rawstr: true, pars: justAddTables ? null : Parameters)); } node = node.Host; continue; } // Just finished the parsing... if (node is DynamicParser.Node.Argument) break; throw new ArgumentException(string.Format("Specification #{0} is invalid: {1}", index, result)); } } else { // Or it is a not supported expression... throw new ArgumentException(string.Format("Specification #{0} is invalid: {1}", index, result)); } // We annotate the aliases being conservative... main = main.Validated("Main"); if (justAddTables) { if (!string.IsNullOrEmpty(main)) tableInfo = tableType == null ? new TableInfo(Database, main, alias, owner, nolock) : new TableInfo(Database, tableType, alias, owner, nolock); else throw new ArgumentException(string.Format("Specification #{0} is invalid: {1}", index, result)); Tables.Add(tableInfo); } else { // Get cached table info tableInfo = string.IsNullOrEmpty(alias) ? Tables.SingleOrDefault(t => t.Name == main && string.IsNullOrEmpty(t.Alias)) : Tables.SingleOrDefault(t => t.Alias == alias); // We finally add the contents if we can... StringBuilder sb = new StringBuilder(); if (string.IsNullOrEmpty(type)) type = "JOIN"; sb.AppendFormat("{0} ", type); if (!string.IsNullOrEmpty(tableInfo.Owner)) sb.AppendFormat("{0}.", Database.DecorateName(tableInfo.Owner)); sb.Append(tableInfo.Name.ContainsAny(StringExtensions.InvalidMemberChars) ? tableInfo.Name : Database.DecorateName(tableInfo.Name)); if (!string.IsNullOrEmpty(tableInfo.Alias)) sb.AppendFormat(" AS {0}", tableInfo.Alias); if (SupportNoLock && tableInfo.NoLock) sb.AppendFormat(" WITH(NOLOCK)"); if (!string.IsNullOrEmpty(condition)) sb.AppendFormat(" ON {0}", condition); _join = string.IsNullOrEmpty(_join) ? sb.ToString() : string.Format("{0} {1}", _join, sb.ToString()); // No comma in this case } } } return this; } #endregion From/Join #region Where /// /// Adds to the 'Where' clause the contents obtained from parsing the dynamic lambda expression given. The condition /// is parsed to the appropriate syntax, where the specific customs virtual methods supported by the parser are used /// as needed. /// - If several Where() methods are chained their contents are, by default, concatenated with an 'AND' operator. /// - The 'And()' and 'Or()' virtual method can be used to concatenate with an 'OR' or an 'AND' operator, as in: /// 'Where( x => x.Or( condition ) )'. /// /// The specification. /// This instance to permit chaining. public virtual IDynamicSelectQueryBuilder Where(Func func) { return this.InternalWhere(func); } /// Add where condition. /// Condition column with operator and value. /// Builder instance. public virtual IDynamicSelectQueryBuilder Where(DynamicColumn column) { return this.InternalWhere(column); } /// Add where condition. /// Condition column. /// Condition operator. /// Condition value. /// Builder instance. public virtual IDynamicSelectQueryBuilder Where(string column, DynamicColumn.CompareOperator op, object value) { return this.InternalWhere(column, op, value); } /// Add where condition. /// Condition column. /// Condition value. /// Builder instance. public virtual IDynamicSelectQueryBuilder Where(string column, object value) { return this.InternalWhere(column, value); } /// Add where condition. /// Set conditions as properties and values of an object. /// If true use schema to determine key columns and ignore those which /// aren't keys. /// Builder instance. public virtual IDynamicSelectQueryBuilder Where(object conditions, bool schema = false) { return this.InternalWhere(conditions, schema); } #endregion Where #region Select /// /// Adds to the 'Select' clause the contents obtained by parsing the dynamic lambda expressions given. The supported /// formats are: /// - Resolve to a string: 'x => "Table.Column AS Alias', where the alias part is optional. /// - Resolve to an expression: 'x => x.Table.Column.As( x.Alias )', where the alias part is optional. /// - Select all columns from a table: 'x => x.Table.All()'. /// - Generic expression: 'x => x( expression ).As( x.Alias )', where the alias part is mandatory. In this case /// the alias is not annotated. /// /// The specification. /// The specification. /// This instance to permit chaining. public virtual IDynamicSelectQueryBuilder Select(Func fn, params Func[] func) { if (fn == null) throw new ArgumentNullException("Array of specifications cannot be null."); int index = SelectFunc(-1, fn); if (func != null) foreach (Func f in func) index = SelectFunc(index, f); return this; } private int SelectFunc(int index, Func f) { index++; if (f == null) throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); using (DynamicParser parser = DynamicParser.Parse(f)) { object result = parser.Result; if (result == null) throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); string main = null; string alias = null; bool all = false; bool anon = false; // If the expression resolves to a string... if (result is string) { string node = (string)result; Tuple tuple = node.SplitSomethingAndAlias(); main = tuple.Item1.Validated("Table and/or Column"); main = FixObjectName(main); alias = tuple.Item2.Validated("Alias", canbeNull: true); } else if (result is DynamicParser.Node) { // Or if it resolves to a dynamic node... ParseSelectNode(result, ref main, ref alias, ref all); } else if (result.GetType().IsAnonymous()) { anon = true; foreach (KeyValuePair prop in result.ToDictionary()) { if (prop.Value is string) { string node = (string)prop.Value; Tuple tuple = node.SplitSomethingAndAlias(); main = FixObjectName(tuple.Item1.Validated("Table and/or Column")); ////alias = tuple.Item2.Validated("Alias", canbeNull: true); } else if (prop.Value is DynamicParser.Node) { // Or if it resolves to a dynamic node... ParseSelectNode(prop.Value, ref main, ref alias, ref all); } else { // Or it is a not supported expression... throw new ArgumentException(string.Format("Specification #{0} in anonymous type is invalid: {1}", index, prop.Value)); } alias = Database.DecorateName(prop.Key); ParseSelectAddColumn(main, alias, all); } } else { // Or it is a not supported expression... throw new ArgumentException(string.Format("Specification #{0} is invalid: {1}", index, result)); } if (!anon) ParseSelectAddColumn(main, alias, all); } return index; } /// Add select columns. /// Columns to add to object. /// Builder instance. public virtual IDynamicSelectQueryBuilder SelectColumn(params DynamicColumn[] columns) { foreach (DynamicColumn col in columns) Select(x => col.ToSQLSelectColumn(Database)); return this; } /// Add select columns. /// Columns to add to object. /// Column format consist of Column Name, Alias and /// Aggregate function in this order separated by ':'. /// Builder instance. public virtual IDynamicSelectQueryBuilder SelectColumn(params string[] columns) { DynamicColumn[] cols = new DynamicColumn[columns.Length]; for (int i = 0; i < columns.Length; i++) cols[i] = DynamicColumn.ParseSelectColumn(columns[i]); return SelectColumn(cols); } #endregion Select #region GroupBy /// /// Adds to the 'Group By' clause the contents obtained from from parsing the dynamic lambda expression given. /// /// The specification. /// The specification. /// This instance to permit chaining. public virtual IDynamicSelectQueryBuilder GroupBy(Func fn, params Func[] func) { if (fn == null) throw new ArgumentNullException("Array of specifications cannot be null."); int index = GroupByFunc(-1, fn); if (func != null) for (int i = 0; i < func.Length; i++) { Func f = func[i]; index = GroupByFunc(index, f); } return this; } private int GroupByFunc(int index, Func f) { index++; if (f == null) throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); using (DynamicParser parser = DynamicParser.Parse(f)) { object result = parser.Result; if (result == null) throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); string main = null; if (result is string) main = FixObjectName(result as string); else main = Parse(result, pars: Parameters); main = main.Validated("Group By"); if (_groupby == null) _groupby = main; else _groupby = string.Format("{0}, {1}", _groupby, main); } return index; } /// Add select columns. /// Columns to group by. /// Builder instance. public virtual IDynamicSelectQueryBuilder GroupByColumn(params DynamicColumn[] columns) { for (int i = 0; i < columns.Length; i++) { DynamicColumn col = columns[i]; GroupBy(x => col.ToSQLGroupByColumn(Database)); } return this; } /// Add select columns. /// Columns to group by. /// Column format consist of Column Name and /// Alias in this order separated by ':'. /// Builder instance. public virtual IDynamicSelectQueryBuilder GroupByColumn(params string[] columns) { return GroupByColumn(columns.Select(c => DynamicColumn.ParseSelectColumn(c)).ToArray()); } #endregion GroupBy #region Having /// /// Adds to the 'Having' clause the contents obtained from parsing the dynamic lambda expression given. The condition /// is parsed to the appropriate syntax, Having the specific customs virtual methods supported by the parser are used /// as needed. /// - If several Having() methods are chained their contents are, by default, concatenated with an 'AND' operator. /// - The 'And()' and 'Or()' virtual method can be used to concatenate with an 'OR' or an 'AND' operator, as in: /// 'Having( x => x.Or( condition ) )'. /// /// The specification. /// This instance to permit chaining. public virtual IDynamicSelectQueryBuilder Having(Func func) { return this.InternalHaving(func); } /// Add Having condition. /// Condition column with operator and value. /// Builder instance. public virtual IDynamicSelectQueryBuilder Having(DynamicColumn column) { return this.InternalHaving(column); } /// Add Having condition. /// Condition column. /// Condition operator. /// Condition value. /// Builder instance. public virtual IDynamicSelectQueryBuilder Having(string column, DynamicColumn.CompareOperator op, object value) { return this.InternalHaving(column, op, value); } /// Add Having condition. /// Condition column. /// Condition value. /// Builder instance. public virtual IDynamicSelectQueryBuilder Having(string column, object value) { return this.InternalHaving(column, value); } /// Add Having condition. /// Set conditions as properties and values of an object. /// If true use schema to determine key columns and ignore those which /// aren't keys. /// Builder instance. public virtual IDynamicSelectQueryBuilder Having(object conditions, bool schema = false) { return this.InternalHaving(conditions, schema); } #endregion Having #region OrderBy /// /// Adds to the 'Order By' clause the contents obtained from from parsing the dynamic lambda expression given. It /// accepts a multipart column specification followed by an optional Ascending() or Descending() virtual methods /// to specify the direction. If no virtual method is used, the default is ascending order. You can also use the /// shorter versions Asc() and Desc(). /// /// The specification. /// The specification. /// This instance to permit chaining. public virtual IDynamicSelectQueryBuilder OrderBy(Func fn, params Func[] func) { if (fn == null) throw new ArgumentNullException("Array of specifications cannot be null."); int index = OrderByFunc(-1, fn); if (func != null) for (int i = 0; i < func.Length; i++) { Func f = func[i]; index = OrderByFunc(index, f); } return this; } private int OrderByFunc(int index, Func f) { index++; if (f == null) throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); using (DynamicParser parser = DynamicParser.Parse(f)) { object result = parser.Result; if (result == null) throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); string main = null; bool ascending = true; if (result is int) main = result.ToString(); else if (result is string) { string[] parts = ((string)result).Split(' '); main = Database.StripName(parts.First()); int colNo; if (!Int32.TryParse(main, out colNo)) main = FixObjectName(main); ascending = parts.Length != 2 || parts.Last().ToUpper() == "ASCENDING" || parts.Last().ToUpper() == "ASC"; } else { // Intercepting trailing 'Ascending' or 'Descending' virtual methods... if (result is DynamicParser.Node.Method) { DynamicParser.Node.Method node = (DynamicParser.Node.Method)result; string name = node.Name.ToUpper(); if (name == "ASCENDING" || name == "ASC" || name == "DESCENDING" || name == "DESC") { object[] args = node.Arguments; if (args != null && !(node.Host is DynamicParser.Node.Argument)) throw new ArgumentException(string.Format("{0} must be a parameterless method, but found: {1}.", name, args.Sketch())); else if ((args == null || args.Length != 1) && node.Host is DynamicParser.Node.Argument) throw new ArgumentException(string.Format("{0} requires one numeric parameter, but found: {1}.", name, args.Sketch())); ascending = (name == "ASCENDING" || name == "ASC") ? true : false; if (args != null && args.Length == 1) { int col = -1; if (args[0] is int) main = args[0].ToString(); else if (args[0] is string) { if (Int32.TryParse(args[0].ToString(), out col)) main = col.ToString(); else main = FixObjectName(args[0].ToString()); } else main = Parse(args[0], pars: Parameters); } result = node.Host; } } // Just parsing the contents... if (!(result is DynamicParser.Node.Argument)) main = Parse(result, pars: Parameters); } main = main.Validated("Order By"); main = string.Format("{0} {1}", main, ascending ? "ASC" : "DESC"); if (_orderby == null) _orderby = main; else _orderby = string.Format("{0}, {1}", _orderby, main); } return index; } /// Add select columns. /// Columns to order by. /// Builder instance. public virtual IDynamicSelectQueryBuilder OrderByColumn(params DynamicColumn[] columns) { for (int i = 0; i < columns.Length; i++) { DynamicColumn col = columns[i]; OrderBy(x => col.ToSQLOrderByColumn(Database)); } return this; } /// Add select columns. /// Columns to order by. /// Column format consist of Column Name and /// Alias in this order separated by ':'. /// Builder instance. public virtual IDynamicSelectQueryBuilder OrderByColumn(params string[] columns) { return OrderByColumn(columns.Select(c => DynamicColumn.ParseOrderByColumn(c)).ToArray()); } #endregion OrderBy #region Top/Limit/Offset/Distinct /// Set top if database support it. /// How many objects select. /// Builder instance. public virtual IDynamicSelectQueryBuilder Top(int? top) { return Limit(top); } /// Set top if database support it. /// How many objects select. /// Builder instance. public virtual IDynamicSelectQueryBuilder Limit(int? limit) { if ((Database.Options & DynamicDatabaseOptions.SupportLimitOffset) != DynamicDatabaseOptions.SupportLimitOffset && (Database.Options & DynamicDatabaseOptions.SupportFirstSkip) != DynamicDatabaseOptions.SupportFirstSkip && (Database.Options & DynamicDatabaseOptions.SupportTop) != DynamicDatabaseOptions.SupportTop) throw new NotSupportedException("Database doesn't support LIMIT clause."); _limit = limit; return this; } /// Set top if database support it. /// How many objects skip selecting. /// Builder instance. public virtual IDynamicSelectQueryBuilder Offset(int? offset) { if ((Database.Options & DynamicDatabaseOptions.SupportLimitOffset) != DynamicDatabaseOptions.SupportLimitOffset && (Database.Options & DynamicDatabaseOptions.SupportFirstSkip) != DynamicDatabaseOptions.SupportFirstSkip) throw new NotSupportedException("Database doesn't support OFFSET clause."); _offset = offset; return this; } /// Set distinct mode. /// Distinct mode. /// Builder instance. public virtual IDynamicSelectQueryBuilder Distinct(bool distinct = true) { _distinct = distinct; return this; } #endregion Top/Limit/Offset/Distinct #region Helpers private void ParseSelectAddColumn(string main, string alias, bool all) { // We annotate the aliases being conservative... main = main.Validated("Main"); ////if (alias != null && !main.ContainsAny(StringExtensions.InvalidMemberChars)) TableAliasList.Add(new KTableAlias(main, alias)); // If all columns are requested... if (all) main += ".*"; // We finally add the contents... string str = (alias == null || all) ? main : string.Format("{0} AS {1}", main, alias); _select = _select == null ? str : string.Format("{0}, {1}", _select, str); } private void ParseSelectNode(object result, ref string column, ref string alias, ref bool all) { string main = null; DynamicParser.Node node = (DynamicParser.Node)result; while (true) { // Support for the AS() virtual method... if (node is DynamicParser.Node.Method && ((DynamicParser.Node.Method)node).Name.ToUpper() == "AS") { if (alias != null) throw new ArgumentException(string.Format("Alias '{0}' is already set when parsing '{1}'.", alias, result)); object[] args = ((DynamicParser.Node.Method)node).Arguments; if (args == null) throw new ArgumentNullException("arg", "AS() is not a parameterless method."); if (args.Length != 1) throw new ArgumentException("AS() requires one and only one parameter: " + args.Sketch()); // Yes, we decorate columns alias = Parse(args[0], rawstr: true, decorate: true, isMultiPart: false).Validated("Alias"); node = node.Host; continue; } // Support for the ALL() virtual method... if (node is DynamicParser.Node.Method && ((DynamicParser.Node.Method)node).Name.ToUpper() == "ALL") { if (all) throw new ArgumentException(string.Format("Flag to select all columns is already set when parsing '{0}'.", result)); object[] args = ((DynamicParser.Node.Method)node).Arguments; if (args != null) throw new ArgumentException("ALL() must be a parameterless virtual method, but found: " + args.Sketch()); all = true; node = node.Host; continue; } // Support for table and/or column specifications... if (node is DynamicParser.Node.GetMember) { if (main != null) throw new ArgumentException(string.Format("Main '{0}' is already set when parsing '{1}'.", main, result)); main = ((DynamicParser.Node.GetMember)node).Name; if (node.Host is DynamicParser.Node.GetMember) { // If leaf then decorate main = Database.DecorateName(main); // Supporting multipart specifications... node = node.Host; // Get table/alias name string table = ((DynamicParser.Node.GetMember)node).Name; bool isAlias = node.Host is DynamicParser.Node.Argument && IsTableAlias(table); if (isAlias) main = string.Format("{0}.{1}", table, main); else if (node.Host is DynamicParser.Node.GetMember) { node = node.Host; main = string.Format("{0}.{1}.{2}", Database.DecorateName(((DynamicParser.Node.GetMember)node).Name), Database.DecorateName(table), main); } else main = string.Format("{0}.{1}", Database.DecorateName(table), main); } else if (node.Host is DynamicParser.Node.Argument) { string table = ((DynamicParser.Node.Argument)node.Host).Name; if (IsTableAlias(table)) main = string.Format("{0}.{1}", table, Database.DecorateName(main)); else if (!IsTableAlias(main)) main = Database.DecorateName(main); } else if (!(node.Host is DynamicParser.Node.Argument && IsTableAlias(main))) main = Database.DecorateName(main); node = node.Host; continue; } // Support for generic sources... if (node is DynamicParser.Node.Invoke) { if (main != null) throw new ArgumentException(string.Format("Main '{0}' is already set when parsing '{1}'.", main, result)); main = string.Format("{0}", Parse(node, rawstr: true, pars: Parameters)); node = node.Host; continue; } // Just finished the parsing... if (node is DynamicParser.Node.Argument) { if (string.IsNullOrEmpty(main) && IsTableAlias(node.Name)) main = node.Name; break; } // All others are assumed to be part of the main element... if (main != null) throw new ArgumentException(string.Format("Main '{0}' is already set when parsing '{1}'.", main, result)); main = Parse(node, pars: Parameters); break; } column = main; } #endregion Helpers #region IExtendedDisposable /// Performs application-defined tasks associated with /// freeing, releasing, or resetting unmanaged resources. public override void Dispose() { base.Dispose(); _select = _from = _join = _groupby = _orderby = null; } #endregion IExtendedDisposable } }