From 6f19ea81bf27dbdf369f2b9d4a36b8c6eb267bb2 Mon Sep 17 00:00:00 2001 From: mkolumb <12272082+mkolumb@users.noreply.github.com> Date: Wed, 19 Jan 2022 03:20:50 +0100 Subject: [PATCH 1/2] Add insert many overload Use dictionary in insert clause Fixed sql server compiler test runner Improved string build performance --- .../Infrastructure/TestCompilersContainer.cs | 20 +++---- QueryBuilder.Tests/InsertTests.cs | 24 +++++++++ QueryBuilder/Clauses/InsertClause.cs | 6 +-- QueryBuilder/Compilers/Compiler.Conditions.cs | 7 +-- QueryBuilder/Compilers/Compiler.cs | 54 ++++++++++--------- .../Extensions/CollectionExtensions.cs | 30 +++++++++++ QueryBuilder/Helper.cs | 8 +-- QueryBuilder/Query.Insert.cs | 44 ++++++++++++--- QueryBuilder/Query.Update.cs | 14 ++--- 9 files changed, 149 insertions(+), 58 deletions(-) create mode 100644 QueryBuilder/Extensions/CollectionExtensions.cs diff --git a/QueryBuilder.Tests/Infrastructure/TestCompilersContainer.cs b/QueryBuilder.Tests/Infrastructure/TestCompilersContainer.cs index c808fb6d..664c9ac4 100644 --- a/QueryBuilder.Tests/Infrastructure/TestCompilersContainer.cs +++ b/QueryBuilder.Tests/Infrastructure/TestCompilersContainer.cs @@ -13,14 +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() + [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 @@ -40,7 +40,7 @@ public Compiler Get(string engineCode) throw new InvalidOperationException(string.Format(Messages.ERR_INVALID_ENGINECODE, engineCode)); } - return Compilers[engineCode]; + return Compilers[engineCode](); } /// @@ -79,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) { @@ -99,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 190a85ea..50228134 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)) @@ -348,7 +350,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; } @@ -392,16 +394,18 @@ protected virtual SqlResult CompileInsertQuery(Query query) var inserts = ctx.Query.GetComponents("insert", EngineCode); + var sql = new StringBuilder(inserts.Count + 1); + if (inserts[0] is InsertClause insertClause) { - var columns = string.Join(", ", WrapArray(insertClause.Columns)); - var values = string.Join(", ", Parameterize(ctx, insertClause.Values)); + var columns = string.Join(", ", WrapArray(insertClause.Data.Keys)); + var values = string.Join(", ", Parameterize(ctx, insertClause.Data.Values)); - ctx.RawSql = $"INSERT INTO {table} ({columns}) VALUES ({values})"; + sql.Append($"INSERT INTO {table} ({columns}) VALUES ({values})"); if (insertClause.ReturnId && !string.IsNullOrEmpty(LastId)) { - ctx.RawSql += ";" + LastId; + sql.Append(";" + LastId); } } else @@ -418,7 +422,7 @@ protected virtual SqlResult CompileInsertQuery(Query query) var subCtx = CompileSelectQuery(clause.Query); ctx.Bindings.AddRange(subCtx.Bindings); - ctx.RawSql = $"INSERT INTO {table}{columns}{subCtx.RawSql}"; + sql.Append($"INSERT INTO {table}{columns}{subCtx.RawSql}"); } if (inserts.Count > 1) @@ -427,11 +431,11 @@ protected virtual SqlResult CompileInsertQuery(Query query) { var clause = insert as InsertClause; - ctx.RawSql += ", (" + string.Join(", ", Parameterize(ctx, clause.Values)) + ")"; - + sql.Append(", (" + string.Join(", ", Parameterize(ctx, clause.Data.Values)) + ")"); } } + ctx.RawSql = sql.ToString().Trim(); return ctx; } @@ -442,7 +446,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) @@ -459,7 +463,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; } @@ -585,7 +589,7 @@ public virtual string CompileUnion(SqlResult ctx) return null; } - var combinedQueries = new List(); + var combinedQueries = new StringBuilder(); var clauses = ctx.Query.GetComponents("combine", EngineCode); @@ -599,7 +603,7 @@ public virtual string CompileUnion(SqlResult ctx) ctx.Bindings.AddRange(subCtx.Bindings); - combinedQueries.Add($"{combineOperator}{subCtx.RawSql}"); + combinedQueries.Append($"{combineOperator}{subCtx.RawSql} "); } else { @@ -607,13 +611,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) @@ -745,7 +747,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) @@ -760,11 +762,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) @@ -958,9 +960,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/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; From 9424ec48babce7546897b82447f7797eeb430f31 Mon Sep 17 00:00:00 2001 From: mkolumb <12272082+mkolumb@users.noreply.github.com> Date: Tue, 28 Mar 2023 21:35:47 +0200 Subject: [PATCH 2/2] Fixing build after merge --- QueryBuilder/Compilers/Compiler.cs | 22 +++++++++++++--------- QueryBuilder/Compilers/OracleCompiler.cs | 19 ++++++++++++------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/QueryBuilder/Compilers/Compiler.cs b/QueryBuilder/Compilers/Compiler.cs index 23d3e9dc..a42d2cde 100644 --- a/QueryBuilder/Compilers/Compiler.cs +++ b/QueryBuilder/Compilers/Compiler.cs @@ -420,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( @@ -437,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})"; @@ -458,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()) 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; } }