diff --git a/DynamORM/Builders/Extensions/DynamicModifyBuilderExtensions.cs b/DynamORM/Builders/Extensions/DynamicModifyBuilderExtensions.cs new file mode 100644 index 0000000..6cdef9c --- /dev/null +++ b/DynamORM/Builders/Extensions/DynamicModifyBuilderExtensions.cs @@ -0,0 +1,82 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012, 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.Linq; +using DynamORM.Builders.Implementation; +using DynamORM.Helpers; +using DynamORM.Mapper; + +namespace DynamORM.Builders.Extensions +{ + internal static class DynamicModifyBuilderExtensions + { + internal static T Table(this T builder, string tableName, Dictionary schema = null) where T : DynamicModifyBuilder + { + var tuple = tableName.Validated("Table Name").SplitSomethingAndAlias(); + + if (!string.IsNullOrEmpty(tuple.Item2)) + throw new ArgumentException(string.Format("Can not use aliases in INSERT steatement. ({0})", tableName), "tableName"); + + var parts = tuple.Item1.Split('.'); + + builder.Tables.Clear(); + builder.Tables.Add(new DynamicQueryBuilder.TableInfo(builder.Database, + builder.Database.StripName(parts.Last()).Validated("Table"), null, + parts.Length == 2 ? builder.Database.StripName(parts.First()).Validated("Owner", canbeNull: true) : null)); + + if (schema != null) + (builder.Tables[0] as DynamicQueryBuilder.TableInfo).Schema = schema; + + return builder; + } + + internal static T Table(this T builder, Type type) where T : DynamicQueryBuilder + { + var mapper = DynamicMapperCache.GetMapper(type); + string name = string.Empty; + + if (mapper == null) + throw new InvalidOperationException("Cant assign unmapable type as a table."); + + if (builder is DynamicModifyBuilder) + { + builder.Tables.Clear(); + builder.Tables.Add(new DynamicQueryBuilder.TableInfo(builder.Database, type)); + } + else if (builder is DynamicSelectQueryBuilder) + (builder as DynamicSelectQueryBuilder).From(x => x(type)); + + return builder; + } + } +} \ No newline at end of file diff --git a/DynamORM/Builders/Extensions/DynamicWhereQueryExtensions.cs b/DynamORM/Builders/Extensions/DynamicWhereQueryExtensions.cs new file mode 100644 index 0000000..1ed4975 --- /dev/null +++ b/DynamORM/Builders/Extensions/DynamicWhereQueryExtensions.cs @@ -0,0 +1,232 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012, 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 DynamORM.Builders.Implementation; +using DynamORM.Helpers; +using DynamORM.Helpers.Dynamics; +using DynamORM.Mapper; + +namespace DynamORM.Builders.Extensions +{ + internal static class DynamicWhereQueryExtensions + { + #region Where + + internal static T InternalWhere(this T builder, Func func) where T : DynamicQueryBuilder, DynamicQueryBuilder.IQueryWithWhere + { + if (func == null) throw new ArgumentNullException("Array of functions cannot be null."); + + using (var parser = DynamicParser.Parse(func)) + { + string condition = null; + bool and = true; + + var result = parser.Result; + if (result is string) + { + condition = (string)result; + + if (condition.ToUpper().IndexOf("OR") == 0) + { + and = false; + condition = condition.Substring(3); + } + else if (condition.ToUpper().IndexOf("AND") == 0) + condition = condition.Substring(4); + } + else if (!(result is DynamicParser.Node) && !result.GetType().IsValueType) + return builder.InternalWhere(result); + else + { + // Intercepting the 'x => x.And()' and 'x => x.Or()' virtual methods... + if (result is DynamicParser.Node.Method && ((DynamicParser.Node.Method)result).Host is DynamicParser.Node.Argument) + { + var node = (DynamicParser.Node.Method)result; + var name = node.Name.ToUpper(); + if (name == "AND" || name == "OR") + { + object[] args = ((DynamicParser.Node.Method)node).Arguments; + if (args == null) throw new ArgumentNullException("arg", string.Format("{0} is not a parameterless method.", name)); + if (args.Length != 1) throw new ArgumentException(string.Format("{0} requires one and only one parameter: {1}.", name, args.Sketch())); + + and = name == "AND" ? true : false; + result = args[0]; + } + } + + // Just parsing the contents now... + condition = builder.Parse(result, pars: builder.Parameters).Validated("Where condition"); + } + + if (builder.WhereCondition == null) builder.WhereCondition = condition; + else builder.WhereCondition = string.Format("{0} {1} {2}", builder.WhereCondition, and ? "AND" : "OR", condition); + } + + return builder; + } + + internal static T InternalWhere(this T builder, DynamicColumn column) where T : DynamicQueryBuilder, DynamicQueryBuilder.IQueryWithWhere + { + builder.VirtualMode = column.VirtualColumn; + bool prepend = false; + + if (column.BeginBlock) + { + if (string.IsNullOrEmpty(builder.WhereCondition)) + prepend = true; + else + builder.WhereCondition += " ("; + } + + // It's kind of uglu, but... well it works. + if (column.Or) + switch (column.Operator) + { + default: + case DynamicColumn.CompareOperator.Eq: builder.InternalWhere(x => x.Or(x(builder.FixObjectName(column.ColumnName)) == column.Value)); break; + case DynamicColumn.CompareOperator.Not: builder.InternalWhere(x => x.Or(x(builder.FixObjectName(column.ColumnName)) != column.Value)); break; + case DynamicColumn.CompareOperator.Like: builder.InternalWhere(x => x.Or(x(builder.FixObjectName(column.ColumnName)).Like(column.Value))); break; + case DynamicColumn.CompareOperator.NotLike: builder.InternalWhere(x => x.Or(x(builder.FixObjectName(column.ColumnName)).NotLike(column.Value))); break; + case DynamicColumn.CompareOperator.In: builder.InternalWhere(x => x.Or(x(builder.FixObjectName(column.ColumnName)).In(column.Value))); break; + case DynamicColumn.CompareOperator.Lt: builder.InternalWhere(x => x.Or(x(builder.FixObjectName(column.ColumnName)) < column.Value)); break; + case DynamicColumn.CompareOperator.Lte: builder.InternalWhere(x => x.Or(x(builder.FixObjectName(column.ColumnName)) <= column.Value)); break; + case DynamicColumn.CompareOperator.Gt: builder.InternalWhere(x => x.Or(x(builder.FixObjectName(column.ColumnName)) > column.Value)); break; + case DynamicColumn.CompareOperator.Gte: builder.InternalWhere(x => x.Or(x(builder.FixObjectName(column.ColumnName)) >= column.Value)); break; + case DynamicColumn.CompareOperator.Between: builder.InternalWhere(x => x.Or(x(builder.FixObjectName(column.ColumnName)).Between(column.Value))); break; + } + else + switch (column.Operator) + { + default: + case DynamicColumn.CompareOperator.Eq: builder.InternalWhere(x => x(builder.FixObjectName(column.ColumnName)) == column.Value); break; + case DynamicColumn.CompareOperator.Not: builder.InternalWhere(x => x(builder.FixObjectName(column.ColumnName)) != column.Value); break; + case DynamicColumn.CompareOperator.Like: builder.InternalWhere(x => x(builder.FixObjectName(column.ColumnName)).Like(column.Value)); break; + case DynamicColumn.CompareOperator.NotLike: builder.InternalWhere(x => x(builder.FixObjectName(column.ColumnName)).NotLike(column.Value)); break; + case DynamicColumn.CompareOperator.In: builder.InternalWhere(x => x(builder.FixObjectName(column.ColumnName)).In(column.Value)); break; + case DynamicColumn.CompareOperator.Lt: builder.InternalWhere(x => x(builder.FixObjectName(column.ColumnName)) < column.Value); break; + case DynamicColumn.CompareOperator.Lte: builder.InternalWhere(x => x(builder.FixObjectName(column.ColumnName)) <= column.Value); break; + case DynamicColumn.CompareOperator.Gt: builder.InternalWhere(x => x(builder.FixObjectName(column.ColumnName)) > column.Value); break; + case DynamicColumn.CompareOperator.Gte: builder.InternalWhere(x => x(builder.FixObjectName(column.ColumnName)) >= column.Value); break; + case DynamicColumn.CompareOperator.Between: builder.InternalWhere(x => x(builder.FixObjectName(column.ColumnName)).Between(column.Value)); break; + } + + if (prepend) + builder.WhereCondition = string.Format("({0}", builder.WhereCondition); + + if (column.EndBlock) + builder.WhereCondition += ")"; + + return builder; + } + + internal static T InternalWhere(this T builder, string column, DynamicColumn.CompareOperator op, object value) where T : DynamicQueryBuilder, DynamicQueryBuilder.IQueryWithWhere + { + if (value is DynamicColumn) + { + var v = (DynamicColumn)value; + + if (string.IsNullOrEmpty(v.ColumnName)) + v.ColumnName = column; + + return builder.InternalWhere(v); + } + else if (value is IEnumerable) + { + foreach (DynamicColumn v in (IEnumerable)value) + builder.InternalWhere(v); + + return builder; + } + + return builder.InternalWhere(new DynamicColumn + { + ColumnName = column, + Operator = op, + Value = value + }); + } + + internal static T InternalWhere(this T builder, string column, object value) where T : DynamicQueryBuilder, DynamicQueryBuilder.IQueryWithWhere + { + return builder.InternalWhere(column, DynamicColumn.CompareOperator.Eq, value); + } + + internal static T InternalWhere(this T builder, object conditions, bool schema = false) where T : DynamicQueryBuilder, DynamicQueryBuilder.IQueryWithWhere + { + if (conditions is DynamicColumn) + return builder.InternalWhere((DynamicColumn)conditions); + else if (conditions is IEnumerable) + { + foreach (DynamicColumn v in (IEnumerable)conditions) + builder.InternalWhere(v); + + return builder; + } + + var dict = conditions.ToDictionary(); + var mapper = DynamicMapperCache.GetMapper(conditions.GetType()); + var table = dict.TryGetValue("_table").NullOr(x => x.ToString(), string.Empty); + + foreach (var condition in dict) + { + if (mapper.Ignored.Contains(condition.Key) || condition.Key == "_table") + continue; + + string colName = mapper != null ? mapper.PropertyMap.TryGetValue(condition.Key) ?? condition.Key : condition.Key; + + DynamicSchemaColumn? col = null; + + // This should be used on typed queries or update/delete steatements, which usualy operate on a single table. + if (schema) + { + col = builder.GetColumnFromSchema(colName, mapper, table); + + if ((!col.HasValue || !col.Value.IsKey) && + (mapper == null || mapper.ColumnsMap.TryGetValue(colName).NullOr(m => m.Ignore || m.Column.NullOr(c => !c.IsKey, true), true))) + continue; + + colName = col.HasValue ? col.Value.Name : colName; + } + + if (!string.IsNullOrEmpty(table)) + builder.InternalWhere(x => x(builder.FixObjectName(string.Format("{0}.{1}", table, colName))) == condition.Value); + else + builder.InternalWhere(x => x(builder.FixObjectName(colName)) == condition.Value); + } + + return builder; + } + + #endregion Where + } +} \ No newline at end of file diff --git a/DynamORM/Builders/Implementation/DynamicDeleteQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicDeleteQueryBuilder.cs new file mode 100644 index 0000000..0c9f71e --- /dev/null +++ b/DynamORM/Builders/Implementation/DynamicDeleteQueryBuilder.cs @@ -0,0 +1,127 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012, 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.Linq; +using DynamORM.Builders.Extensions; + +namespace DynamORM.Builders.Implementation +{ + /// Implementation of dynamic delete query builder. + internal class DynamicDeleteQueryBuilder : DynamicModifyBuilder, IDynamicDeleteQueryBuilder, DynamicQueryBuilder.IQueryWithWhere + { + /// + /// Initializes a new instance of the class. + /// + /// The database. + internal DynamicDeleteQueryBuilder(DynamicDatabase db) + : base(db) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The database. + /// Name of the table. + public DynamicDeleteQueryBuilder(DynamicDatabase db, string tableName) + : base(db, tableName) + { + } + + /// Generates the text this command will execute against the underlying database. + /// The text to execute against the underlying database. + /// This method must be override by derived classes. + public override string CommandText() + { + var info = Tables.Single(); + return string.Format("DELETE FROM {0}{1} WHERE {2}", + string.IsNullOrEmpty(info.Owner) ? string.Empty : string.Format("{0}.", Database.DecorateName(info.Owner)), + Database.DecorateName(info.Name), WhereCondition); + } + + #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 IDynamicDeleteQueryBuilder Where(Func func) + { + return this.InternalWhere(func); + } + + /// Add where condition. + /// Condition column with operator and value. + /// Builder instance. + public virtual IDynamicDeleteQueryBuilder Where(DynamicColumn column) + { + return this.InternalWhere(column); + } + + /// Add where condition. + /// Condition column. + /// Condition operator. + /// Condition value. + /// Builder instance. + public virtual IDynamicDeleteQueryBuilder 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 IDynamicDeleteQueryBuilder 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 IDynamicDeleteQueryBuilder Where(object conditions, bool schema = false) + { + return this.InternalWhere(conditions, schema); + } + + #endregion Where + } +} \ No newline at end of file diff --git a/DynamORM/Builders/Implementation/DynamicInsertQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicInsertQueryBuilder.cs new file mode 100644 index 0000000..4879cdb --- /dev/null +++ b/DynamORM/Builders/Implementation/DynamicInsertQueryBuilder.cs @@ -0,0 +1,201 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012, 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.Linq; +using DynamORM.Helpers.Dynamics; +using DynamORM.Mapper; + +namespace DynamORM.Builders.Implementation +{ + /// Implementation of dynamic insert query builder. + internal class DynamicInsertQueryBuilder : DynamicModifyBuilder, IDynamicInsertQueryBuilder + { + private string _columns; + private string _values; + + /// + /// Initializes a new instance of the class. + /// + /// The database. + internal DynamicInsertQueryBuilder(DynamicDatabase db) + : base(db) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The database. + /// Name of the table. + public DynamicInsertQueryBuilder(DynamicDatabase db, string tableName) + : base(db, tableName) + { + } + + /// Generates the text this command will execute against the underlying database. + /// The text to execute against the underlying database. + /// This method must be override by derived classes. + public override string CommandText() + { + var info = Tables.Single(); + return string.Format("INSERT INTO {0}{1} ({2}) VALUES ({3})", + string.IsNullOrEmpty(info.Owner) ? string.Empty : string.Format("{0}.", Database.DecorateName(info.Owner)), + Database.DecorateName(info.Name), _columns, _values); + } + + #region Insert + + /// + /// Specifies the columns to insert using the dynamic lambda expressions given. Each expression correspond to one + /// column, and can: + /// - Resolve to a string, in this case a '=' must appear in the string. + /// - Resolve to a expression with the form: 'x => x.Column = Value'. + /// + /// The specifications. + /// This instance to permit chaining. + public virtual IDynamicInsertQueryBuilder Insert(params Func[] func) + { + if (func == null) + throw new ArgumentNullException("Array of specifications cannot be null."); + + int index = -1; + + foreach (var f in func) + { + index++; + + if (f == null) + throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); + + using (var parser = DynamicParser.Parse(f)) + { + var result = parser.Result; + if (result == null) + throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); + + string main = null; + string value = null; + string str = null; + + // When 'x => x.Table.Column = value' or 'x => x.Column = value'... + if (result is DynamicParser.Node.SetMember) + { + var node = (DynamicParser.Node.SetMember)result; + + DynamicSchemaColumn? col = GetColumnFromSchema(node.Name); + main = Database.DecorateName(node.Name); + value = Parse(node.Value, pars: Parameters, nulls: true, columnSchema: col); + + _columns = _columns == null ? main : string.Format("{0}, {1}", _columns, main); + _values = _values == null ? value : string.Format("{0}, {1}", _values, value); + continue; + } + else if (!(result is DynamicParser.Node) && !result.GetType().IsValueType) + { + Insert(result); + continue; + } + + // Other specifications are considered invalid... + var err = string.Format("Specification '{0}' is invalid.", result); + str = Parse(result); + if (str.Contains("=")) err += " May have you used a '==' instead of a '=' operator?"; + throw new ArgumentException(err); + } + } + + return this; + } + + /// Add insert fields. + /// Insert column and value. + /// Builder instance. + public virtual IDynamicInsertQueryBuilder Insert(DynamicColumn column) + { + DynamicSchemaColumn? col = column.Schema ?? GetColumnFromSchema(column.ColumnName); + + string main = FixObjectName(column.ColumnName, onlyColumn: true); + string value = Parse(column.Value, pars: Parameters, nulls: true, columnSchema: col); + + _columns = _columns == null ? main : string.Format("{0}, {1}", _columns, main); + _values = _values == null ? value : string.Format("{0}, {1}", _values, value); + + return this; + } + + /// Add insert fields. + /// Insert column. + /// Insert value. + /// Builder instance. + public virtual IDynamicInsertQueryBuilder Insert(string column, object value) + { + if (value is DynamicColumn) + { + var v = (DynamicColumn)value; + + if (string.IsNullOrEmpty(v.ColumnName)) + v.ColumnName = column; + + return Insert(v); + } + + return Insert(new DynamicColumn + { + ColumnName = column, + Value = value, + }); + } + + /// Add insert fields. + /// Set insert value as properties and values of an object. + /// Builder instance. + public virtual IDynamicInsertQueryBuilder Insert(object o) + { + var dict = o.ToDictionary(); + var mapper = DynamicMapperCache.GetMapper(o.GetType()); + + if (mapper != null) + { + foreach (var con in dict) + if (!mapper.Ignored.Contains(con.Key)) + Insert(mapper.PropertyMap.TryGetValue(con.Key) ?? con.Key, con.Value); + } + else + foreach (var con in dict) + Insert(con.Key, con.Value); + + return this; + } + + #endregion Insert + } +} \ No newline at end of file diff --git a/DynamORM/Builders/Implementation/DynamicModifyBuilder.cs b/DynamORM/Builders/Implementation/DynamicModifyBuilder.cs new file mode 100644 index 0000000..9fb8cd9 --- /dev/null +++ b/DynamORM/Builders/Implementation/DynamicModifyBuilder.cs @@ -0,0 +1,71 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + * + * 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 DynamORM.Builders.Extensions; + +namespace DynamORM.Builders.Implementation +{ + /// Base query builder for insert/update/delete statements. + internal abstract class DynamicModifyBuilder : DynamicQueryBuilder + { + /// + /// Initializes a new instance of the class. + /// + /// The database. + public DynamicModifyBuilder(DynamicDatabase db) + : base(db) + { + VirtualMode = false; + } + + /// + /// Initializes a new instance of the class. + /// + /// The database. + /// Name of the table. + public DynamicModifyBuilder(DynamicDatabase db, string tableName) + : this(db) + { + VirtualMode = false; + this.Table(tableName); + } + + /// Execute this builder. + /// Result of an execution.. + public virtual int Execute() + { + using (var con = Database.Open()) + using (var cmd = con.CreateCommand()) + { + return cmd + .SetCommand(this) + .ExecuteNonQuery(); + } + } + } +} \ No newline at end of file diff --git a/DynamORM/Builders/Implementation/DynamicQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicQueryBuilder.cs new file mode 100644 index 0000000..a653f75 --- /dev/null +++ b/DynamORM/Builders/Implementation/DynamicQueryBuilder.cs @@ -0,0 +1,744 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012, 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.Linq.Expressions; +using System.Text; +using DynamORM.Helpers; +using DynamORM.Helpers.Dynamics; +using DynamORM.Mapper; + +namespace DynamORM.Builders.Implementation +{ + /// Implementation of dynamic query builder base interface. + internal abstract class DynamicQueryBuilder : IDynamicQueryBuilder + { + /// Empty interface to allow where query builder implementation use universal approach. + internal interface IQueryWithWhere { } + + private DynamicQueryBuilder _parent = null; + + /// Gets or sets a value indicating whether add virtual. + internal bool VirtualMode { get; set; } + + #region TableInfo + + /// Table information. + internal class TableInfo : ITableInfo + { + /// + /// Initializes a new instance of the class. + /// + internal TableInfo() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The database. + /// The name of table. + /// The table alias. + /// The table owner. + public TableInfo(DynamicDatabase db, string name, string alias = null, string owner = null) + { + Name = name; + Alias = alias; + Owner = owner; + + if (!name.ContainsAny(StringExtensions.InvalidMemberChars)) + Schema = db.GetSchema(name, owner: owner); + } + + /// + /// Initializes a new instance of the class. + /// + /// The database. + /// The type which can be mapped to database. + /// The table alias. + /// The table owner. + public TableInfo(DynamicDatabase db, Type type, string alias = null, string owner = null) + { + var mapper = DynamicMapperCache.GetMapper(type); + + Name = mapper.Table == null || string.IsNullOrEmpty(mapper.Table.Name) ? + mapper.Type.Name : mapper.Table.Name; + + Owner = (mapper.Table != null) ? mapper.Table.Owner : owner; + Alias = alias; + + Schema = db.GetSchema(type); + } + + /// Gets or sets table owner name. + public string Owner { get; internal set; } + + /// Gets or sets table name. + public string Name { get; internal set; } + + /// Gets or sets table alias. + public string Alias { get; internal set; } + + /// Gets or sets table schema. + public Dictionary Schema { get; internal set; } + } + + /// Generic based table information. + /// Type of class that is represented in database. + internal class TableInfo : TableInfo + { + /// + /// Initializes a new instance of the class. + /// + /// The database. + /// The table alias. + /// The table owner. + public TableInfo(DynamicDatabase db, string alias = null, string owner = null) + : base(db, typeof(T), alias, owner) + { + } + } + + #endregion TableInfo + + #region Parameter + + /// Interface describing parameter info. + internal class Parameter : IParameter + { + /// Gets or sets the parameter temporary name. + public string Name { get; internal set; } + + /// Gets or sets the parameter value. + public object Value { get; set; } + + /// Gets or sets a value indicating whether this is virtual. + public bool Virtual { get; set; } + + /// Gets or sets the parameter schema information. + public DynamicSchemaColumn? Schema { get; internal set; } + } + + #endregion Parameter + + internal string WhereCondition { get; set; } + + /// Gets instance. + public DynamicDatabase Database { get; private set; } + + /// Gets the tables used in this builder. + public IList Tables { get; private set; } + + /// Gets the tables used in this builder. + public IDictionary Parameters { get; private set; } + + /// Gets a value indicating whether database supports standard schema. + public bool SupportSchema { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The database. + public DynamicQueryBuilder(DynamicDatabase db) + { + VirtualMode = false; + Tables = new List(); + Parameters = new Dictionary(); + + Database = db; + SupportSchema = (db.Options & DynamicDatabaseOptions.SupportSchema) == DynamicDatabaseOptions.SupportSchema; + } + + /// Initializes a new instance of the class. + /// The database. + /// The parent query. + internal DynamicQueryBuilder(DynamicDatabase db, DynamicQueryBuilder parent) + : this(db) + { + _parent = parent; + } + + internal bool IsTableAlias(string name) + { + DynamicQueryBuilder builder = this; + + while (builder != null) + { + if (builder.Tables.Any(t => t.Alias == name)) + return true; + + builder = builder._parent; + } + + return false; + } + + internal bool IsTable(string name, string owner) + { + DynamicQueryBuilder builder = this; + + while (builder != null) + { + if ((string.IsNullOrEmpty(owner) && builder.Tables.Any(t => t.Name.ToLower() == name.ToLower())) || + (!string.IsNullOrEmpty(owner) && builder.Tables.Any(t => t.Name.ToLower() == name.ToLower() && + !string.IsNullOrEmpty(t.Owner) && t.Owner.ToLower() == owner.ToLower()))) + return true; + + builder = builder._parent; + } + + return false; + } + + /// Creates sub query. + /// Sub query builder. + public IDynamicSelectQueryBuilder SubQuery() + { + return new DynamicSelectQueryBuilder(Database, this); + } + + /// Adds to the 'From' clause of sub query 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. + /// This instance to permit chaining. + public IDynamicSelectQueryBuilder SubQuery(params Func[] func) + { + return SubQuery().From(func); + } + + /// + /// Generates the text this command will execute against the underlying database. + /// + /// The text to execute against the underlying database. + /// This method must be override by derived classes. + public abstract string CommandText(); + + /// Fill command with query. + /// Command to fill. + /// Filled instance of . + public virtual IDbCommand FillCommand(IDbCommand command) + { + return command.SetCommand(CommandText() + .FillStringWithVariables(s => + { + return Parameters.TryGetValue(s).NullOr(p => + { + return ((IDbDataParameter)command + .AddParameter(this, p.Schema, p.Value) + .Parameters[command.Parameters.Count - 1]) + .ParameterName; + }, s); + })); + } + + internal DynamicSchemaColumn? GetColumnFromSchema(string colName, DynamicTypeMap mapper = null, string table = null) + { + // This is tricky and will not always work unfortunetly. + if (colName.ContainsAny(StringExtensions.InvalidMultipartMemberChars)) + return null; + + // First we need to get real column name and it's owner if exist. + var parts = colName.Split('.') + .Select(c => Database.StripName(c)) + .ToArray(); + + var columnName = parts.Last(); + + // Get table name from mapper + string tableName = table; + + if (string.IsNullOrEmpty(tableName)) + { + tableName = (mapper != null && mapper.Table != null) ? mapper.Table.Name : string.Empty; + + if (parts.Length > 1 && string.IsNullOrEmpty(tableName)) + { + // OK, we have a multi part identifier, that's good, we can get table name + tableName = string.Join(".", parts.Take(parts.Length - 1)); + } + } + + // Try to get table info from cache + var tableInfo = !string.IsNullOrEmpty(tableName) ? + Tables.FirstOrDefault(x => !string.IsNullOrEmpty(x.Alias) && x.Alias.ToLower() == tableName) ?? + Tables.FirstOrDefault(x => x.Name.ToLower() == tableName.ToLower()) ?? Tables.FirstOrDefault() : + this is DynamicModifyBuilder ? Tables.FirstOrDefault() : null; + + // Try to get column from schema + if (tableInfo != null && tableInfo.Schema != null) + return tableInfo.Schema.TryGetNullable(columnName.ToLower()); + + // Well, we failed to find a column + return null; + } + + #region Parser + + /// Parses the arbitrary object given and translates it into a string with the appropriate + /// syntax for the database this parser is specific to. + /// The object to parse and translate. It can be any arbitrary object, including null values (if + /// permitted) and dynamic lambda expressions. + /// If not null, the parameters' list where to store the parameters extracted by the parsing. + /// If true, literal (raw) string are allowed. If false and the node is a literal then, as a + /// security measure, an exception is thrown. + /// True to accept null values and translate them into the appropriate syntax accepted by the + /// database. If false and the value is null, then an exception is thrown. + /// If set to true decorate element. + /// If set parse argument as alias. This is workaround for AS method. + /// This parameter is used to determine type of parameter used in query. + /// A string containing the result of the parsing, along with the parameters extracted in the + /// instance if such is given. + /// Null nodes are not accepted. + internal virtual string Parse(object node, IDictionary pars = null, bool rawstr = false, bool nulls = false, bool decorate = true, bool isMultiPart = true, DynamicSchemaColumn? columnSchema = null) + { + // Null nodes are accepted or not depending upon the "nulls" flag... + if (node == null) + { + if (!nulls) + throw new ArgumentNullException("node", "Null nodes are not accepted."); + + return Dispatch(node, pars, decorate); + } + + // Nodes that are strings are parametrized or not depending the "rawstr" flag... + if (node is string) + { + if (rawstr) return (string)node; + else return Dispatch(node, pars, decorate); + } + + // If node is a delegate, parse it to create the logical tree... + if (node is Delegate) + { + node = DynamicParser.Parse((Delegate)node).Result; + return Parse(node, pars, rawstr, decorate: decorate, columnSchema: columnSchema); // Intercept containers as in (x => "string") + } + + return Dispatch(node, pars, decorate, isMultiPart, columnSchema); + } + + private string Dispatch(object node, IDictionary pars = null, bool decorate = true, bool isMultiPart = true, DynamicSchemaColumn? columnSchema = null) + { + if (node != null) + { + if (node is DynamicQueryBuilder) return ParseCommand((DynamicQueryBuilder)node, pars); + else if (node is DynamicParser.Node.Argument) return ParseArgument((DynamicParser.Node.Argument)node, isMultiPart); + else if (node is DynamicParser.Node.GetMember) return ParseGetMember((DynamicParser.Node.GetMember)node, pars, decorate, isMultiPart, columnSchema); + else if (node is DynamicParser.Node.SetMember) return ParseSetMember((DynamicParser.Node.SetMember)node, pars, decorate, isMultiPart, columnSchema); + else if (node is DynamicParser.Node.Unary) return ParseUnary((DynamicParser.Node.Unary)node, pars); + else if (node is DynamicParser.Node.Binary) return ParseBinary((DynamicParser.Node.Binary)node, pars); + else if (node is DynamicParser.Node.Method) return ParseMethod((DynamicParser.Node.Method)node, pars); + else if (node is DynamicParser.Node.Invoke) return ParseInvoke((DynamicParser.Node.Invoke)node, pars); + else if (node is DynamicParser.Node.Convert) return ParseConvert((DynamicParser.Node.Convert)node, pars); + } + + // All other cases are considered constant parameters... + return ParseConstant(node, pars, columnSchema); + } + + protected virtual string ParseCommand(DynamicQueryBuilder node, IDictionary pars = null) + { + // Getting the command's text... + string str = node.CommandText(); // Avoiding spurious "OUTPUT XXX" statements + + // If there are parameters to transform, but cannot store them, it is an error + if (node.Parameters.Count != 0 && pars == null) + throw new InvalidOperationException(string.Format("The parameters in this command '{0}' cannot be added to a null collection.", node.Parameters)); + + // Copy parameters to new comand + foreach (var parameter in node.Parameters) + pars.Add(parameter.Key, parameter.Value); + + return string.Format("({0})", str); + } + + protected virtual string ParseArgument(DynamicParser.Node.Argument node, bool isMultiPart = true, bool isOwner = false) + { + if (!string.IsNullOrEmpty(node.Name) && (isOwner || (isMultiPart && IsTableAlias(node.Name)))) + return node.Name; + + return null; + } + + protected virtual string ParseGetMember(DynamicParser.Node.GetMember node, IDictionary pars = null, bool decorate = true, bool isMultiPart = true, DynamicSchemaColumn? columnSchema = null) + { + if (node.Host is DynamicParser.Node.Argument && IsTableAlias(node.Name)) + { + decorate = false; + isMultiPart = false; + } + + // This hack allows to use argument as alias, but when it is not nesesary use other column. + // Let say we hace a table Users with alias usr, and we join to table with alias ua which also has a column Users + // This allow use of usr => usr.ua.Users to result in ua."Users" instead of "Users" or usr."ua"."Users", se tests for examples. + string parent = null; + if (node.Host != null) + { + if (isMultiPart && node.Host is DynamicParser.Node.GetMember && IsTable(node.Host.Name, null)) + { + if (node.Host.Host != null && node.Host.Host is DynamicParser.Node.GetMember && IsTable(node.Host.Name, node.Host.Host.Name)) + parent = string.Format("{0}.{1}", Parse(node.Host.Host, pars, isMultiPart: false), Parse(node.Host, pars, isMultiPart: false)); + else + parent = Parse(node.Host, pars, isMultiPart: false); + } + else if (isMultiPart) + parent = Parse(node.Host, pars, isMultiPart: isMultiPart); + } + + ////string parent = node.Host == null || !isMultiPart ? null : Parse(node.Host, pars, isMultiPart: !IsTable(node.Name, node.Host.Name)); + string name = parent == null ? + decorate ? Database.DecorateName(node.Name) : node.Name : + string.Format("{0}.{1}", parent, decorate ? Database.DecorateName(node.Name) : node.Name); + + columnSchema = GetColumnFromSchema(name); + + return name; + } + + protected virtual string ParseSetMember(DynamicParser.Node.SetMember node, IDictionary pars = null, bool decorate = true, bool isMultiPart = true, DynamicSchemaColumn? columnSchema = null) + { + if (node.Host is DynamicParser.Node.Argument && IsTableAlias(node.Name)) + { + decorate = false; + isMultiPart = false; + } + + string parent = null; + if (node.Host != null) + { + if (isMultiPart && node.Host is DynamicParser.Node.GetMember && IsTable(node.Host.Name, null)) + { + if (node.Host.Host != null && node.Host.Host is DynamicParser.Node.GetMember && IsTable(node.Name, node.Host.Name)) + parent = string.Format("{0}.{1}", Parse(node.Host.Host, pars, isMultiPart: false), Parse(node.Host, pars, isMultiPart: false)); + else + parent = Parse(node.Host, pars, isMultiPart: false); + } + else if (isMultiPart) + parent = Parse(node.Host, pars, isMultiPart: isMultiPart); + } + + ////string parent = node.Host == null || !isMultiPart ? null : Parse(node.Host, pars, isMultiPart: !IsTable(node.Name, node.Host.Name)); + string name = parent == null ? + decorate ? Database.DecorateName(node.Name) : node.Name : + string.Format("{0}.{1}", parent, decorate ? Database.DecorateName(node.Name) : node.Name); + + columnSchema = GetColumnFromSchema(name); + + string value = Parse(node.Value, pars, nulls: true, columnSchema: columnSchema); + return string.Format("{0} = ({1})", name, value); + } + + protected virtual string ParseUnary(DynamicParser.Node.Unary node, IDictionary pars = null) + { + switch (node.Operation) + { + // Artifacts from the DynamicParser class that are not usefull here... + case ExpressionType.IsFalse: + case ExpressionType.IsTrue: return Parse(node.Target, pars); + + // Unary supported operations... + case ExpressionType.Not: return string.Format("(NOT {0})", Parse(node.Target, pars)); + case ExpressionType.Negate: return string.Format("!({0})", Parse(node.Target, pars)); + } + + throw new ArgumentException("Not supported unary operation: " + node); + } + + protected virtual string ParseBinary(DynamicParser.Node.Binary node, IDictionary pars = null) + { + string op = string.Empty; + + switch (node.Operation) + { + // Arithmetic binary operations... + case ExpressionType.Add: op = "+"; break; + case ExpressionType.Subtract: op = "-"; break; + case ExpressionType.Multiply: op = "*"; break; + case ExpressionType.Divide: op = "/"; break; + case ExpressionType.Modulo: op = "%"; break; + case ExpressionType.Power: op = "^"; break; + + case ExpressionType.And: op = "AND"; break; + case ExpressionType.Or: op = "OR"; break; + + // Logical comparisons... + case ExpressionType.GreaterThan: op = ">"; break; + case ExpressionType.GreaterThanOrEqual: op = ">="; break; + case ExpressionType.LessThan: op = "<"; break; + case ExpressionType.LessThanOrEqual: op = "<="; break; + + // Comparisons against 'NULL' require the 'IS' or 'IS NOT' operator instead the numeric ones... + case ExpressionType.Equal: op = node.Right == null && !VirtualMode ? "IS" : "="; break; + case ExpressionType.NotEqual: op = node.Right == null && !VirtualMode ? "IS NOT" : "<>"; break; + + default: throw new ArgumentException("Not supported operator: '" + node.Operation); + } + + DynamicSchemaColumn? columnSchema = null; + string left = Parse(node.Left, pars, columnSchema: columnSchema); // Not nulls: left is assumed to be an object + string right = Parse(node.Right, pars, nulls: true, columnSchema: columnSchema); + return string.Format("({0} {1} {2})", left, op, right); + } + + protected virtual string ParseMethod(DynamicParser.Node.Method node, IDictionary pars = null) + { + string method = node.Name.ToUpper(); + string parent = node.Host == null ? null : Parse(node.Host, pars: pars); + string item = null; + + // Root-level methods... + if (node.Host == null) + { + switch (method) + { + case "NOT": + if (node.Arguments == null || node.Arguments.Length != 1) throw new ArgumentNullException("NOT method expects one argument: " + node.Arguments.Sketch()); + item = Parse(node.Arguments[0], pars: pars); + return string.Format("(NOT {0})", item); + } + } + + // Column-level methods... + if (node.Host != null) + { + switch (method) + { + case "BETWEEN": + { + if (node.Arguments == null || node.Arguments.Length == 0) + throw new ArgumentException("BETWEEN method expects at least one argument: " + node.Arguments.Sketch()); + + if (node.Arguments.Length > 2) + throw new ArgumentException("BETWEEN method expects at most two arguments: " + node.Arguments.Sketch()); + + var arguments = node.Arguments; + + if (arguments.Length == 1 && (arguments[0] is IEnumerable || arguments[0] is Array) && !(arguments[0] is byte[])) + { + var vals = arguments[0] as IEnumerable; + + if (vals == null && arguments[0] is Array) + vals = ((Array)arguments[0]).Cast() as IEnumerable; + + if (vals != null) + arguments = vals.ToArray(); + else + throw new ArgumentException("BETWEEN method expects single argument to be enumerable of exactly two elements: " + node.Arguments.Sketch()); + } + + return string.Format("{0} BETWEEN {1} AND {2}", parent, Parse(arguments[0], pars: pars), Parse(arguments[1], pars: pars)); + } + + case "IN": + { + if (node.Arguments == null || node.Arguments.Length == 0) + throw new ArgumentException("IN method expects at least one argument: " + node.Arguments.Sketch()); + + bool firstParam = true; + StringBuilder sbin = new StringBuilder(); + foreach (var arg in node.Arguments) + { + if (!firstParam) + sbin.Append(", "); + + if ((arg is IEnumerable || arg is Array) && !(arg is byte[])) + { + var vals = arg as IEnumerable; + + if (vals == null && arg is Array) + vals = ((Array)arg).Cast() as IEnumerable; + + if (vals != null) + foreach (var val in vals) + { + if (!firstParam) + sbin.Append(", "); + else + firstParam = false; + + sbin.Append(Parse(val, pars: pars)); + } + else + sbin.Append(Parse(arg, pars: pars)); + } + else + sbin.Append(Parse(arg, pars: pars)); + + firstParam = false; + } + + return string.Format("{0} IN({1})", parent, sbin.ToString()); + } + + case "LIKE": + if (node.Arguments == null || node.Arguments.Length != 1) + throw new ArgumentException("LIKE method expects one argument: " + node.Arguments.Sketch()); + + return string.Format("{0} LIKE {1}", parent, Parse(node.Arguments[0], pars: pars)); + + case "NOTLIKE": + if (node.Arguments == null || node.Arguments.Length != 1) + throw new ArgumentException("NOT LIKE method expects one argument: " + node.Arguments.Sketch()); + + return string.Format("{0} NOT LIKE {1}", parent, Parse(node.Arguments[0], pars: pars)); + + case "AS": + if (node.Arguments == null || node.Arguments.Length != 1) + throw new ArgumentException("AS method expects one argument: " + node.Arguments.Sketch()); + + item = Parse(node.Arguments[0], pars: null, rawstr: true, isMultiPart: false); // pars=null to avoid to parameterize aliases + item = item.Validated("Alias"); // Intercepting null and empty aliases + return string.Format("{0} AS {1}", parent, item); + + case "COUNT": + if (node.Arguments != null && node.Arguments.Length > 1) + throw new ArgumentException("COUNT method expects one or none argument: " + node.Arguments.Sketch()); + + if (node.Arguments == null || node.Arguments.Length == 0) + return "COUNT(*)"; + + return string.Format("COUNT({0})", Parse(node.Arguments[0], pars: Parameters, nulls: true)); + } + } + + // Default case: parsing the method's name along with its arguments... + method = parent == null ? node.Name : string.Format("{0}.{1}", parent, node.Name); + StringBuilder sb = new StringBuilder(); + sb.AppendFormat("{0}(", method); + + if (node.Arguments != null && node.Arguments.Length != 0) + { + bool first = true; + + foreach (object argument in node.Arguments) + { + if (!first) + sb.Append(", "); + else + first = false; + + sb.Append(Parse(argument, pars, nulls: true)); // We don't accept raw strings here!!! + } + } + + sb.Append(")"); + return sb.ToString(); + } + + protected virtual string ParseInvoke(DynamicParser.Node.Invoke node, IDictionary pars = null) + { + // This is used as an especial syntax to merely concatenate its arguments. It is used as a way to extend the supported syntax without the need of treating all the possible cases... + if (node.Arguments == null || node.Arguments.Length == 0) + return string.Empty; + + StringBuilder sb = new StringBuilder(); + foreach (object arg in node.Arguments) + { + if (arg is string) + sb.Append((string)arg); + else + sb.Append(Parse(arg, pars, rawstr: true, nulls: true)); + } + + return sb.ToString(); + } + + protected virtual string ParseConvert(DynamicParser.Node.Convert node, IDictionary pars = null) + { + // The cast mechanism is left for the specific database implementation, that should override this method + // as needed... + string r = Parse(node.Target, pars); + return r; + } + + protected virtual string ParseConstant(object node, IDictionary pars = null, DynamicSchemaColumn? columnSchema = null) + { + if (node == null) return ParseNull(); + + if (pars != null) + { + // If we have a list of parameters to store it, let's parametrize it + var name = Guid.NewGuid().ToString(); + var par = new Parameter() + { + Name = string.Format("[${0}]", name), + Value = node, + Virtual = VirtualMode, + Schema = columnSchema, + }; + + pars.Add(name, par); + + return par.Name; + } + + return node.ToString(); // Last resort case + } + + protected virtual string ParseNull() + { + return "NULL"; // Override if needed + } + + internal string FixObjectName(string main, bool onlyColumn = false) + { + if (main.IndexOf("(") > 0 && main.IndexOf(")") > 0) + return main.FillStringWithVariables(f => string.Format("({0})", FixObjectNamePrivate(f, onlyColumn)), "(", ")"); + else + return FixObjectNamePrivate(main, onlyColumn); + } + + private string FixObjectNamePrivate(string f, bool onlyColumn = false) + { + var objects = f.Split('.') + .Select(x => Database.StripName(x)); + + if (onlyColumn || objects.Count() == 1) + f = Database.DecorateName(objects.Last()); + else if (!IsTableAlias(objects.First())) + f = string.Join(".", objects.Select(o => Database.DecorateName(o))); + else + f = string.Format("{0}.{1}", objects.First(), string.Join(".", objects.Skip(1).Select(o => Database.DecorateName(o)))); + + return f; + } + + #endregion Parser + } +} \ No newline at end of file diff --git a/DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs new file mode 100644 index 0000000..e7e4da6 --- /dev/null +++ b/DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs @@ -0,0 +1,1238 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012, 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.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 + { + private int? _top = null; + private int? _limit = null; + private int? _offset = null; + private bool _distinct = false; + + private string _select; + private string _from; + private string _join; + private string _groupby; + private string _orderby; + + /// + /// 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() + { + StringBuilder sb = new StringBuilder("SELECT"); + if (_distinct) sb.AppendFormat(" DISTINCT"); + if (_top.HasValue) sb.AppendFormat(" TOP {0}", _top); + 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 (_orderby != null) sb.AppendFormat(" ORDER BY {0}", _orderby); + if (_limit.HasValue) sb.AppendFormat(" LIMIT {0}", _limit); + if (_offset.HasValue) sb.AppendFormat(" OFFSET {0}", _offset); + + return sb.ToString(); + } + + #region Execution + + /*/// Execute this builder. + + /// Enumerator of objects expanded from query. + public virtual IEnumerator GetEnumerator() + { + using (var con = Database.Open()) + using (var cmd = con.CreateCommand()) + { + using (var rdr = cmd + .SetCommand(this) + .ExecuteReader()) + while (rdr.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 = rdr.RowToDynamic(); + } + catch (ArgumentException argex) + { + var 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. + /// Enumerator of objects expanded from query. + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + }*/ + + /// Execute this builder. + /// Enumerator of objects expanded from query. + public virtual IEnumerable Execute() + { + using (var con = Database.Open()) + using (var cmd = con.CreateCommand()) + { + using (var rdr = cmd + .SetCommand(this) + .ExecuteReader()) + while (rdr.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 = rdr.RowToDynamic(); + } + catch (ArgumentException argex) + { + var 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 + { + var mapper = DynamicMapperCache.GetMapper(); + + if (mapper == null) + throw new InvalidOperationException("Type can't be mapped for unknown reason."); + + using (var con = Database.Open()) + using (var cmd = con.CreateCommand()) + { + using (var rdr = cmd + .SetCommand(this) + .ExecuteReader()) + while (rdr.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 = rdr.RowToDynamic(); + } + catch (ArgumentException argex) + { + var 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); + } + } + } + + /// Returns a single result. + /// Result of a query. + public virtual object Scalar() + { + using (var con = Database.Open()) + using (var cmd = con.CreateCommand()) + { + return cmd + .SetCommand(this) + .ExecuteScalar(); + } + } + + #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. + /// This instance to permit chaining. + public virtual IDynamicSelectQueryBuilder From(params Func[] func) + { + if (func == null) throw new ArgumentNullException("Array of functions cannot be null."); + + int index = -1; + foreach (var f in func) + { + index++; + ITableInfo tableInfo = null; + using (var parser = DynamicParser.Parse(f)) + { + var result = parser.Result; + + // If the expression result is string. + if (result is string) + { + var node = (string)result; + var tuple = node.SplitSomethingAndAlias(); + var 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 DynamicParser.Node) + { + // Or if it resolves to a dynamic node + var node = (DynamicParser.Node)result; + + string owner = null; + string main = null; + string alias = null; + 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 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 + { + var invoke = (DynamicParser.Node.Invoke)node; + if (invoke.Arguments.Length == 1 && invoke.Arguments[0] is Type) + { + type = (Type)invoke.Arguments[0]; + var mapper = DynamicMapperCache.GetMapper(type); + + if (mapper == null) + throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}).", type.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: 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) : new TableInfo(Database, type, alias, owner); + 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); + + _from = string.IsNullOrEmpty(_from) ? sb.ToString() : string.Format("{0}, {1}", _from, sb.ToString()); + } + } + + return this; + } + + /// + /// 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 (var f in func) + { + index++; + ITableInfo tableInfo = null; + + if (f == null) + throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); + + using (var parser = DynamicParser.Parse(f)) + { + var 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; + Type tableType = null; + + // If the expression resolves to a string... + if (result is string) + { + var 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(); + } + + var tuple = main.SplitSomethingAndAlias(); // In this case we split on the remaining 'main' + var 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... + var 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 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) + { + 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]); + var 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 + { + var invoke = (DynamicParser.Node.Invoke)node; + if (invoke.Arguments.Length == 1 && invoke.Arguments[0] is Type) + { + tableType = (Type)invoke.Arguments[0]; + var 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) : new TableInfo(Database, tableType, alias, owner); + 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 (!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. + /// This instance to permit chaining. + public virtual IDynamicSelectQueryBuilder Select(params Func[] func) + { + if (func == null) + throw new ArgumentNullException("Array of specifications cannot be null."); + + int index = -1; + foreach (var f in func) + { + index++; + if (f == null) + throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); + + using (var parser = DynamicParser.Parse(f)) + { + var 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) + { + var node = (string)result; + var 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 (var prop in result.ToDictionary()) + { + if (prop.Value is string) + { + var node = (string)prop.Value; + var 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 this; + } + + /// Add select columns. + /// Columns to add to object. + /// Builder instance. + public virtual IDynamicSelectQueryBuilder Select(params DynamicColumn[] columns) + { + foreach (var 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 Select(params string[] columns) + { + return Select(columns.Select(c => DynamicColumn.ParseSelectColumn(c)).ToArray()); + } + + #endregion Select + + #region GroupBy + + /// + /// Adds to the 'Group By' clause the contents obtained from from parsing the dynamic lambda expression given. + /// + /// The specification. + /// This instance to permit chaining. + public virtual IDynamicSelectQueryBuilder GroupBy(params Func[] func) + { + if (func == null) + throw new ArgumentNullException("Array of specifications cannot be null."); + + int index = -1; + + foreach (var f in func) + { + index++; + if (f == null) + throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); + using (var parser = DynamicParser.Parse(f)) + { + var 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 this; + } + + /// Add select columns. + /// Columns to group by. + /// Builder instance. + public virtual IDynamicSelectQueryBuilder GroupBy(params DynamicColumn[] columns) + { + foreach (var col in columns) + 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 GroupBy(params string[] columns) + { + return GroupBy(columns.Select(c => DynamicColumn.ParseSelectColumn(c)).ToArray()); + } + + #endregion GroupBy + + #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. + /// This instance to permit chaining. + public virtual IDynamicSelectQueryBuilder OrderBy(params Func[] func) + { + if (func == null) + throw new ArgumentNullException("Array of specifications cannot be null."); + + int index = -1; + + foreach (var f in func) + { + index++; + if (f == null) + throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); + + using (var parser = DynamicParser.Parse(f)) + { + var 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) + { + var 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) + { + var node = (DynamicParser.Node.Method)result; + var 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 this; + } + + /// Add select columns. + /// Columns to order by. + /// Builder instance. + public virtual IDynamicSelectQueryBuilder OrderBy(params DynamicColumn[] columns) + { + foreach (var col in columns) + 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 OrderBy(params string[] columns) + { + return OrderBy(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) + { + if ((Database.Options & DynamicDatabaseOptions.SupportTop) != DynamicDatabaseOptions.SupportTop) + throw new NotSupportedException("Database doesn't support TOP clause."); + + _top = top; + return this; + } + + /// 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) + 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) + 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; + + var 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 + var 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) + { + var 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 + } +} \ No newline at end of file diff --git a/DynamORM/Builders/Implementation/DynamicUpdateQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicUpdateQueryBuilder.cs new file mode 100644 index 0000000..7b41d50 --- /dev/null +++ b/DynamORM/Builders/Implementation/DynamicUpdateQueryBuilder.cs @@ -0,0 +1,324 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012, 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.Linq; +using DynamORM.Builders.Extensions; +using DynamORM.Helpers.Dynamics; +using DynamORM.Mapper; + +namespace DynamORM.Builders.Implementation +{ + /// Update query builder. + internal class DynamicUpdateQueryBuilder : DynamicModifyBuilder, IDynamicUpdateQueryBuilder, DynamicQueryBuilder.IQueryWithWhere + { + private string _columns; + + internal DynamicUpdateQueryBuilder(DynamicDatabase db) + : base(db) + { + } + + public DynamicUpdateQueryBuilder(DynamicDatabase db, string tableName) + : base(db, tableName) + { + } + + /// Generates the text this command will execute against the underlying database. + /// The text to execute against the underlying database. + /// This method must be override by derived classes. + public override string CommandText() + { + var info = Tables.Single(); + return string.Format("UPDATE {0}{1} SET {2} WHERE {3}", + string.IsNullOrEmpty(info.Owner) ? string.Empty : string.Format("{0}.", Database.DecorateName(info.Owner)), + Database.DecorateName(info.Name), _columns, WhereCondition); + } + + #region Update + + /// Add update value or where condition using schema. + /// Update or where column name and value. + /// Builder instance. + public virtual IDynamicUpdateQueryBuilder Update(DynamicColumn column) + { + DynamicSchemaColumn? col = column.Schema ?? GetColumnFromSchema(column.ColumnName); + + if (!col.HasValue && SupportSchema) + throw new InvalidOperationException(string.Format("Column '{0}' not found in schema, can't use universal approach.", column)); + + if (col.HasValue && col.Value.IsKey) + Where(column); + else + Values(column.ColumnName, column.Value); + + return this; + } + + /// Add update value or where condition using schema. + /// Update or where column name. + /// Column value. + /// Builder instance. + public virtual IDynamicUpdateQueryBuilder Update(string column, object value) + { + DynamicSchemaColumn? col = GetColumnFromSchema(column); + + if (!col.HasValue && SupportSchema) + throw new InvalidOperationException(string.Format("Column '{0}' not found in schema, can't use universal approach.", column)); + + if (col.HasValue && col.Value.IsKey) + Where(column, value); + else + Values(column, value); + + return this; + } + + /// Add update values and where condition columns using schema. + /// Set values or conditions as properties and values of an object. + /// Builder instance. + public virtual IDynamicUpdateQueryBuilder Update(object conditions) + { + if (conditions is DynamicColumn) + return Update((DynamicColumn)conditions); + + var dict = conditions.ToDictionary(); + var mapper = DynamicMapperCache.GetMapper(conditions.GetType()); + + foreach (var con in dict) + { + if (mapper.Ignored.Contains(con.Key)) + continue; + + string colName = mapper != null ? mapper.PropertyMap.TryGetValue(con.Key) ?? con.Key : con.Key; + DynamicSchemaColumn? col = GetColumnFromSchema(colName); + + if (!col.HasValue && SupportSchema) + throw new InvalidOperationException(string.Format("Column '{0}' not found in schema, can't use universal approach.", colName)); + + if (col.HasValue) + { + colName = col.Value.Name; + + if (col.Value.IsKey) + { + Where(colName, con.Value); + + continue; + } + } + + Values(colName, con.Value); + } + + return this; + } + + #endregion Update + + #region Values + + /// + /// Specifies the columns to update using the dynamic lambda expressions given. Each expression correspond to one + /// column, and can: + /// - Resolve to a string, in this case a '=' must appear in the string. + /// - Resolve to a expression with the form: 'x => x.Column = Value'. + /// + /// The specifications. + /// This instance to permit chaining. + public virtual IDynamicUpdateQueryBuilder Values(params Func[] func) + { + if (func == null) + throw new ArgumentNullException("Array of specifications cannot be null."); + + int index = -1; + foreach (var f in func) + { + index++; + if (f == null) + throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); + var result = DynamicParser.Parse(f).Result; + + if (result == null) + throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); + + string main = null; + string value = null; + string str = null; + + // When 'x => x.Table.Column = value' or 'x => x.Column = value'... + if (result is DynamicParser.Node.SetMember) + { + var node = (DynamicParser.Node.SetMember)result; + + DynamicSchemaColumn? col = GetColumnFromSchema(node.Name); + main = Database.DecorateName(node.Name); + value = Parse(node.Value, pars: Parameters, nulls: true, columnSchema: col); + + str = string.Format("{0} = {1}", main, value); + _columns = _columns == null ? str : string.Format("{0}, {1}", _columns, str); + continue; + } + else if (!(result is DynamicParser.Node) && !result.GetType().IsValueType) + { + Values(result); + continue; + } + + // Other specifications are considered invalid... + var err = string.Format("Specification '{0}' is invalid.", result); + str = Parse(result); + if (str.Contains("=")) err += " May have you used a '==' instead of a '=' operator?"; + throw new ArgumentException(err); + } + + return this; + } + + /// Add insert fields. + /// Insert column and value. + /// Builder instance. + public virtual IDynamicUpdateQueryBuilder Values(DynamicColumn column) + { + DynamicSchemaColumn? col = column.Schema ?? GetColumnFromSchema(column.ColumnName); + + string main = FixObjectName(column.ColumnName, onlyColumn: true); + string value = Parse(column.Value, pars: Parameters, nulls: true, columnSchema: col); + + var str = string.Format("{0} = {1}", main, value); + _columns = _columns == null ? str : string.Format("{0}, {1}", _columns, str); + + return this; + } + + /// Add insert fields. + /// Insert column. + /// Insert value. + /// Builder instance. + public virtual IDynamicUpdateQueryBuilder Values(string column, object value) + { + if (value is DynamicColumn) + { + var v = (DynamicColumn)value; + + if (string.IsNullOrEmpty(v.ColumnName)) + v.ColumnName = column; + + return Values(v); + } + + return Values(new DynamicColumn + { + ColumnName = column, + Value = value, + }); + } + + /// Add insert fields. + /// Set insert value as properties and values of an object. + /// Builder instance. + public virtual IDynamicUpdateQueryBuilder Values(object o) + { + var dict = o.ToDictionary(); + var mapper = DynamicMapperCache.GetMapper(o.GetType()); + + if (mapper != null) + { + foreach (var con in dict) + if (!mapper.Ignored.Contains(con.Key)) + Values(mapper.PropertyMap.TryGetValue(con.Key) ?? con.Key, con.Value); + } + else + foreach (var con in dict) + Values(con.Key, con.Value); + + return this; + } + + #endregion Values + + #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 IDynamicUpdateQueryBuilder Where(Func func) + { + return this.InternalWhere(func); + } + + /// Add where condition. + /// Condition column with operator and value. + /// Builder instance. + public virtual IDynamicUpdateQueryBuilder Where(DynamicColumn column) + { + return this.InternalWhere(column); + } + + /// Add where condition. + /// Condition column. + /// Condition operator. + /// Condition value. + /// Builder instance. + public virtual IDynamicUpdateQueryBuilder 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 IDynamicUpdateQueryBuilder 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 IDynamicUpdateQueryBuilder Where(object conditions, bool schema = false) + { + return this.InternalWhere(conditions, schema); + } + + #endregion Where + } +} \ No newline at end of file