diff --git a/QueryBuilder.Tests/Infrastructure/TestCompilersContainer.cs b/QueryBuilder.Tests/Infrastructure/TestCompilersContainer.cs index 2312a192..664c9ac4 100644 --- a/QueryBuilder.Tests/Infrastructure/TestCompilersContainer.cs +++ b/QueryBuilder.Tests/Infrastructure/TestCompilersContainer.cs @@ -13,17 +13,14 @@ private static class Messages public const string ERR_INVALID_ENGINECODES = "Invalid engine codes supplied '{0}'"; } - protected readonly IDictionary Compilers = new Dictionary + protected readonly IDictionary> Compilers = new Dictionary> { - [EngineCodes.Firebird] = new FirebirdCompiler(), - [EngineCodes.MySql] = new MySqlCompiler(), - [EngineCodes.Oracle] = new OracleCompiler(), - [EngineCodes.PostgreSql] = new PostgresCompiler(), - [EngineCodes.Sqlite] = new SqliteCompiler(), - [EngineCodes.SqlServer] = new SqlServerCompiler() - { - UseLegacyPagination = true - } + [EngineCodes.Firebird] = () => new FirebirdCompiler(), + [EngineCodes.MySql] = () => new MySqlCompiler(), + [EngineCodes.Oracle] = () => new OracleCompiler(), + [EngineCodes.PostgreSql] = () => new PostgresCompiler(), + [EngineCodes.Sqlite] = () => new SqliteCompiler(), + [EngineCodes.SqlServer] = () => new SqlServerCompiler { UseLegacyPagination = true } }; public IEnumerable KnownEngineCodes @@ -43,7 +40,7 @@ public Compiler Get(string engineCode) throw new InvalidOperationException(string.Format(Messages.ERR_INVALID_ENGINECODE, engineCode)); } - return Compilers[engineCode]; + return Compilers[engineCode](); } /// @@ -82,7 +79,7 @@ public TestSqlResultContainer Compile(IEnumerable engineCodes, Query que var results = Compilers .Where(w => codes.Contains(w.Key)) - .ToDictionary(k => k.Key, v => v.Value.Compile(query.Clone())); + .ToDictionary(k => k.Key, v => v.Value().Compile(query.Clone())); if (results.Count != codes.Count) { @@ -102,7 +99,7 @@ public TestSqlResultContainer Compile(IEnumerable engineCodes, Query que public TestSqlResultContainer Compile(Query query) { var resultKeyValues = Compilers - .ToDictionary(k => k.Key, v => v.Value.Compile(query.Clone())); + .ToDictionary(k => k.Key, v => v.Value().Compile(query.Clone())); return new TestSqlResultContainer(resultKeyValues); } } diff --git a/QueryBuilder.Tests/InsertTests.cs b/QueryBuilder.Tests/InsertTests.cs index 926e18b2..034b1f67 100644 --- a/QueryBuilder.Tests/InsertTests.cs +++ b/QueryBuilder.Tests/InsertTests.cs @@ -99,6 +99,30 @@ public void InsertMultiRecords() c[EngineCodes.Firebird]); } + [Fact] + public void InsertMultiRecordsByDictionary() + { + var data = new List> + { + new() { { "name", "Chiron" }, { "brand", "Bugatti" }, { "year", null } }, + new() { { "name", "Huayra" }, { "brand", "Pagani" }, { "year", 2012 } }, + new() { { "name", "Reventon roadster" }, { "brand", "Lamborghini" }, { "year", 2009 } } + }; + + var query = new Query("expensive_cars") + .AsInsert(data); + + var c = Compile(query); + + Assert.Equal( + "INSERT INTO [expensive_cars] ([name], [brand], [year]) VALUES ('Chiron', 'Bugatti', NULL), ('Huayra', 'Pagani', 2012), ('Reventon roadster', 'Lamborghini', 2009)", + c[EngineCodes.SqlServer]); + + Assert.Equal( + "INSERT INTO \"EXPENSIVE_CARS\" (\"NAME\", \"BRAND\", \"YEAR\") SELECT 'Chiron', 'Bugatti', NULL FROM RDB$DATABASE UNION ALL SELECT 'Huayra', 'Pagani', 2012 FROM RDB$DATABASE UNION ALL SELECT 'Reventon roadster', 'Lamborghini', 2009 FROM RDB$DATABASE", + c[EngineCodes.Firebird]); + } + [Fact] public void InsertWithNullValues() { diff --git a/QueryBuilder/Clauses/InsertClause.cs b/QueryBuilder/Clauses/InsertClause.cs index 14ece2d2..1afd8355 100644 --- a/QueryBuilder/Clauses/InsertClause.cs +++ b/QueryBuilder/Clauses/InsertClause.cs @@ -9,8 +9,7 @@ public abstract class AbstractInsertClause : AbstractClause public class InsertClause : AbstractInsertClause { - public List Columns { get; set; } - public List Values { get; set; } + public Dictionary Data { get; set; } public bool ReturnId { get; set; } = false; public override AbstractClause Clone() @@ -19,8 +18,7 @@ public override AbstractClause Clone() { Engine = Engine, Component = Component, - Columns = Columns, - Values = Values, + Data = Data, ReturnId = ReturnId, }; } diff --git a/QueryBuilder/Compilers/Compiler.Conditions.cs b/QueryBuilder/Compilers/Compiler.Conditions.cs index 6615c246..174c6710 100644 --- a/QueryBuilder/Compilers/Compiler.Conditions.cs +++ b/QueryBuilder/Compilers/Compiler.Conditions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text; namespace SqlKata.Compilers { @@ -43,7 +44,7 @@ protected virtual string CompileCondition(SqlResult ctx, AbstractCondition claus protected virtual string CompileConditions(SqlResult ctx, List conditions) { - var result = new List(); + var result = new StringBuilder(); for (var i = 0; i < conditions.Count; i++) { @@ -56,10 +57,10 @@ protected virtual string CompileConditions(SqlResult ctx, List("update", EngineCode); - var parts = new List(); + var parts = new StringBuilder(toUpdate.Data.Count); - for (var i = 0; i < toUpdate.Columns.Count; i++) + var separator = ", "; + + foreach (var item in toUpdate.Data) { - parts.Add(Wrap(toUpdate.Columns[i]) + " = " + Parameter(ctx, toUpdate.Values[i])); + parts.Append(Wrap(item.Key) + " = " + Parameter(ctx, item.Value) + separator); } - var sets = string.Join(", ", parts); - + parts.Length -= separator.Length; + wheres = CompileWheres(ctx); if (!string.IsNullOrEmpty(wheres)) @@ -383,7 +385,7 @@ protected virtual SqlResult CompileUpdateQuery(Query query) wheres = " " + wheres; } - ctx.RawSql = $"UPDATE {table} SET {sets}{wheres}"; + ctx.RawSql = $"UPDATE {table} SET {parts}{wheres}".Trim(); return ctx; } @@ -418,7 +420,7 @@ protected virtual SqlResult CompileInsertQuery(Query query) if (inserts[0] is InsertQueryClause insertQueryClause) return CompileInsertQueryClause(ctx, table, insertQueryClause); else - return CompileValueInsertClauses(ctx, table, inserts.Cast()); + return CompileValueInsertClauses(ctx, table, inserts.Cast().ToArray()); } protected virtual SqlResult CompileInsertQueryClause( @@ -435,15 +437,15 @@ protected virtual SqlResult CompileInsertQueryClause( } protected virtual SqlResult CompileValueInsertClauses( - SqlResult ctx, string table, IEnumerable insertClauses) + SqlResult ctx, string table, IReadOnlyCollection insertClauses) { - bool isMultiValueInsert = insertClauses.Skip(1).Any(); + bool isMultiValueInsert = insertClauses.Count > 1; var insertInto = (isMultiValueInsert) ? MultiInsertStartClause : SingleInsertStartClause; var firstInsert = insertClauses.First(); - string columns = GetInsertColumnsList(firstInsert.Columns); - var values = string.Join(", ", Parameterize(ctx, firstInsert.Values)); + string columns = GetInsertColumnsList(firstInsert.Data.Keys); + var values = string.Join(", ", Parameterize(ctx, firstInsert.Data.Values)); ctx.RawSql = $"{insertInto} {table}{columns} VALUES ({values})"; @@ -456,17 +458,21 @@ protected virtual SqlResult CompileValueInsertClauses( return ctx; } - protected virtual SqlResult CompileRemainingInsertClauses(SqlResult ctx, string table, IEnumerable inserts) + protected virtual SqlResult CompileRemainingInsertClauses(SqlResult ctx, string table, IReadOnlyCollection inserts) { + var sql = new StringBuilder(ctx.RawSql, inserts.Count - 1); + foreach (var insert in inserts.Skip(1)) { - string values = string.Join(", ", Parameterize(ctx, insert.Values)); - ctx.RawSql += $", ({values})"; + sql.Append($", ({string.Join(", ", Parameterize(ctx, insert.Data.Values))})"); } + + ctx.RawSql = sql.ToString(); + return ctx; } - protected string GetInsertColumnsList(List columnList) + protected string GetInsertColumnsList(IReadOnlyCollection columnList) { var columns = ""; if (columnList.Any()) @@ -480,7 +486,7 @@ protected virtual SqlResult CompileCteQuery(SqlResult ctx, Query query) var cteFinder = new CteFinder(query, EngineCode); var cteSearchResult = cteFinder.Find(); - var rawSql = new StringBuilder("WITH "); + var rawSql = new StringBuilder("WITH ", cteSearchResult.Count * 2 + 3); var cteBindings = new List(); foreach (var cte in cteSearchResult) @@ -497,7 +503,7 @@ protected virtual SqlResult CompileCteQuery(SqlResult ctx, Query query) rawSql.Append(ctx.RawSql); ctx.Bindings.InsertRange(0, cteBindings); - ctx.RawSql = rawSql.ToString(); + ctx.RawSql = rawSql.ToString().Trim(); return ctx; } @@ -657,7 +663,7 @@ public virtual string CompileUnion(SqlResult ctx) return null; } - var combinedQueries = new List(); + var combinedQueries = new StringBuilder(); var clauses = ctx.Query.GetComponents("combine", EngineCode); @@ -671,7 +677,7 @@ public virtual string CompileUnion(SqlResult ctx) ctx.Bindings.AddRange(subCtx.Bindings); - combinedQueries.Add($"{combineOperator}{subCtx.RawSql}"); + combinedQueries.Append($"{combineOperator}{subCtx.RawSql} "); } else { @@ -679,13 +685,11 @@ public virtual string CompileUnion(SqlResult ctx) ctx.Bindings.AddRange(combineRawClause.Bindings); - combinedQueries.Add(WrapIdentifiers(combineRawClause.Expression)); - + combinedQueries.Append(WrapIdentifiers(combineRawClause.Expression) + " "); } } - return string.Join(" ", combinedQueries); - + return combinedQueries.ToString().Trim(); } public virtual string CompileTableExpression(SqlResult ctx, AbstractFrom from) @@ -817,7 +821,7 @@ public virtual string CompileHaving(SqlResult ctx) return null; } - var sql = new List(); + var sql = new StringBuilder(); string boolOperator; var having = ctx.Query.GetComponents("having", EngineCode) @@ -832,11 +836,11 @@ public virtual string CompileHaving(SqlResult ctx) { boolOperator = i > 0 ? having[i].IsOr ? "OR " : "AND " : ""; - sql.Add(boolOperator + compiled); + sql.Append(boolOperator + compiled + " "); } } - return $"HAVING {string.Join(" ", sql)}"; + return $"HAVING {sql}".Trim(); } public virtual string CompileLimit(SqlResult ctx) @@ -1042,9 +1046,9 @@ public virtual string Parameterize(SqlResult ctx, IEnumerable values) /// /// /// - public virtual List WrapArray(List values) + public virtual IEnumerable WrapArray(IEnumerable values) { - return values.Select(x => Wrap(x)).ToList(); + return values.Select(Wrap); } public virtual string WrapIdentifiers(string input) diff --git a/QueryBuilder/Compilers/OracleCompiler.cs b/QueryBuilder/Compilers/OracleCompiler.cs index 610ec20d..747d3132 100644 --- a/QueryBuilder/Compilers/OracleCompiler.cs +++ b/QueryBuilder/Compilers/OracleCompiler.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Text; using System.Text.RegularExpressions; namespace SqlKata.Compilers @@ -156,20 +157,24 @@ protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCond } protected override SqlResult CompileRemainingInsertClauses( - SqlResult ctx, string table, IEnumerable inserts) + SqlResult ctx, string table, IReadOnlyCollection inserts) { + var sql = new StringBuilder(ctx.RawSql, inserts.Count - 1); + foreach (var insert in inserts.Skip(1)) { - string columns = GetInsertColumnsList(insert.Columns); - string values = string.Join(", ", Parameterize(ctx, insert.Values)); + string columns = GetInsertColumnsList(insert.Data.Keys); + string values = string.Join(", ", Parameterize(ctx, insert.Data.Values)); - string intoFormat = " INTO {0}{1} VALUES ({2})"; - var nextInsert = string.Format(intoFormat, table, columns, values); + const string intoFormat = " INTO {0}{1} VALUES ({2})"; - ctx.RawSql += nextInsert; + sql.Append(string.Format(intoFormat, table, columns, values)); } - ctx.RawSql += " SELECT 1 FROM DUAL"; + sql.Append(" SELECT 1 FROM DUAL"); + + ctx.RawSql = sql.ToString(); + return ctx; } } diff --git a/QueryBuilder/Extensions/CollectionExtensions.cs b/QueryBuilder/Extensions/CollectionExtensions.cs new file mode 100644 index 00000000..3d420c5c --- /dev/null +++ b/QueryBuilder/Extensions/CollectionExtensions.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Linq; + +namespace SqlKata.Extensions +{ + public static class CollectionExtensions + { + public static Dictionary MergeKeysAndValues(this List keys, List values) + { + var data = new Dictionary(); + + for (var i = 0; i < keys.Count; i++) + { + data.Add(keys[i], values[i]); + } + + return data; + } + + public static Dictionary CreateDictionary(this IEnumerable> values) + { + if (values is Dictionary dictionary) + { + return dictionary; + } + + return values.ToDictionary(x => x.Key, x => x.Value); + } + } +} diff --git a/QueryBuilder/Helper.cs b/QueryBuilder/Helper.cs index 218a95e3..4749f5a3 100644 --- a/QueryBuilder/Helper.cs +++ b/QueryBuilder/Helper.cs @@ -96,14 +96,16 @@ public static string ReplaceAll(string subject, string match, Func public static string JoinArray(string glue, IEnumerable array) { - var result = new List(); + var result = new StringBuilder(); foreach (var item in array) { - result.Add(item.ToString()); + result.Append(item + glue); } - return string.Join(glue, result); + result.Length -= glue.Length; + + return result.ToString().Trim(); } public static string ExpandParameters(string sql, string placeholder, object[] bindings) diff --git a/QueryBuilder/Query.Insert.cs b/QueryBuilder/Query.Insert.cs index dbec60af..e49f1b51 100644 --- a/QueryBuilder/Query.Insert.cs +++ b/QueryBuilder/Query.Insert.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using SqlKata.Extensions; namespace SqlKata { @@ -33,8 +34,7 @@ public Query AsInsert(IEnumerable columns, IEnumerable values) ClearComponent("insert").AddComponent("insert", new InsertClause { - Columns = columnsList, - Values = valuesList + Data = columnsList.MergeKeysAndValues(valuesList) }); return this; @@ -51,8 +51,7 @@ public Query AsInsert(IEnumerable> values, bool ret ClearComponent("insert").AddComponent("insert", new InsertClause { - Columns = values.Select(x => x.Key).ToList(), - Values = values.Select(x => x.Value).ToList(), + Data = values.CreateDictionary(), ReturnId = returnId, }); @@ -89,8 +88,41 @@ public Query AsInsert(IEnumerable columns, IEnumerable + /// Produces insert multi records + /// + /// + /// + public Query AsInsert(IEnumerable>> data) + { + if (data == null || !data.Any()) + { + throw new InvalidOperationException($"{nameof(data)} cannot be null or empty"); + } + + Method = "insert"; + + ClearComponent("insert"); + + foreach (var item in data) + { + var row = item.CreateDictionary(); + + if (row.Keys.Count != row.Values.Count) + { + throw new InvalidOperationException($"{nameof(row.Keys)} count should be equal to each {nameof(row.Values)} entry count"); + } + + AddComponent("insert", new InsertClause + { + Data = row }); } diff --git a/QueryBuilder/Query.Update.cs b/QueryBuilder/Query.Update.cs index d88aeb00..0350e62e 100644 --- a/QueryBuilder/Query.Update.cs +++ b/QueryBuilder/Query.Update.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using SqlKata.Extensions; namespace SqlKata { @@ -16,12 +17,15 @@ public Query AsUpdate(object data) public Query AsUpdate(IEnumerable columns, IEnumerable values) { - if ((columns?.Any() ?? false) == false || (values?.Any() ?? false) == false) + var columnsList = columns?.ToList(); + var valuesList = values?.ToList(); + + if ((columnsList?.Count ?? 0) == 0 || (valuesList?.Count ?? 0) == 0) { throw new InvalidOperationException($"{columns} and {values} cannot be null or empty"); } - if (columns.Count() != values.Count()) + if (columnsList.Count != valuesList.Count) { throw new InvalidOperationException($"{columns} count should be equal to {values} count"); } @@ -30,8 +34,7 @@ public Query AsUpdate(IEnumerable columns, IEnumerable values) ClearComponent("update").AddComponent("update", new InsertClause { - Columns = columns.ToList(), - Values = values.ToList() + Data = columnsList.MergeKeysAndValues(valuesList) }); return this; @@ -48,8 +51,7 @@ public Query AsUpdate(IEnumerable> values) ClearComponent("update").AddComponent("update", new InsertClause { - Columns = values.Select(x => x.Key).ToList(), - Values = values.Select(x => x.Value).ToList(), + Data = values.CreateDictionary() }); return this;