From 2f75345aa2936be99e4e1d09dec5bd3c4dc7e1dd Mon Sep 17 00:00:00 2001 From: Thomas Franey Date: Mon, 4 Aug 2025 13:44:13 -0500 Subject: [PATCH 1/3] Issue-757: added Query update with CASE WHEN capabilities --- NewReadMe.md | 112 +++++++++++++++++++++ QueryBuilder/BaseQuery.cs | 2 + QueryBuilder/Compilers/Compiler.cs | 150 ++++++++++++++++++++++++++++- QueryBuilder/Constants.cs | 27 ++++++ SqlKata.Execution/QueryFactory.cs | 3 +- 5 files changed, 291 insertions(+), 3 deletions(-) create mode 100644 NewReadMe.md create mode 100644 QueryBuilder/Constants.cs diff --git a/NewReadMe.md b/NewReadMe.md new file mode 100644 index 00000000..2146d3bf --- /dev/null +++ b/NewReadMe.md @@ -0,0 +1,112 @@ +* New Update and UpdateAsync upgrade: CASE + +** A new feature added to allow developers to programmatically set CASE WHEN when assigning values. Feature includes grouping in sub statements () or +** to allow condition to point to a column variable instead of a direct paramater value. SQL injection friendly + +** Original Update Statement for multiple records using anonymous objects: + +*** foreach (var item in data) + +*** { + +*** object obj = new + +*** { + +*** MyField = item.Value + +*** }; + +*** cnt += await QueryFactory.Query(tableName).Where("Id", item.Id).UpdateAsync(value); + + +*** } + +*** return cnt; + + + + + +** New Update with select case using multi-level array systems +** version 1 : allows is equal condition only for now +** For the Else it will always fill with name of field itself , self assigning. +** This happens if format is wrong as well. +** The else protects you fro your field to be set back to NULL + +*** Warning: Limitation is requires , Suggest 200 rows for low number columns, +*** 25 for higher number columns or clauses. + + + var datac = data.Chunk(200); // breaking data up to 200 rows + + //each holds for each update set, which allows multiple value setting as older Update + List cases = []; + + if (datac.Any()) foreach (var d in datac) + { + + try + { + foreach (var item in d) //Build case when statement , standard 3 + { + cases.Add(["Id", item.Id, item.Value]); + } + object obj = new + { + MyField= cases.ToArray() + }; + cases.Clear(); + + //if data set is smaller than whole table , best to use in statement to reduce cost + cnt += await QueryFactory.Query(tableName) + .WhereIn("Id", d.Select(dd => dd.Id).ToArray()) + .UpdateAsync(value); + } + catch { throw; } + finally { cases.Clear(); } + } + else cases.Clear(); + + return cnt; + + + + +**standard: Case WHEN x = A then Y... END: +*** In your cases array the flow is [x,A,Y]. +*** Assignmet value is always last. + + + + + +** Available Feaure 1 : While its common to do 3 items for basic, when can extend the criteria with AND and OR +** It combine, the array column after the orevioud criteria field must be an AND or OR, unless using , () or * explained later + +*** Note: Assignmet value is always last. you can use AND,&&,& or OR,||,|, <>. Not case sensitive. + +*** Case WHEN x = A AND z = B then Y ... END: +*** In your cases array the flow is [x,A,"AND",z,B,Y] +*** Case WHEN x = A OR z = B then Y ... END: +*** Array the flow is [x,A,"OR",z,B,Y] + + + + + +** Available Feaure 2 : Subset (). This allows seperating your "And" & "Or" blocks +*** ex: case when (a = 1 or a = 5) and (b = 7 and c = 2) +*** This can be placed anywhere before the assignment column or * assignment column, +*** if you forget to add the ) to close, the engine +*** will compensate. + +*** Case WHEN (x = A AND z = B) OR J = C then Y ... END: +*** Array the flow is ["(",x,A,"AND",z,B,")","OR",j,c,Y] +*** Case WHEN (x = A OR z = B) AND (J = C AND K = D) then Y ... END: +*** Array the flow is ["(",x,A,"OR",z,B,")","AND","(",j,c,"AND",k,d,")" Y] + + +** Available Feaure 3 : To Another Column Field (*). This allows criteria to check if column equals another column (field) +*** Case WHEN (colx = colb AND colz = colx) then Y ... END: +*** Array the flow is [,colx,*',colb,"AND",colz,colx, Y] diff --git a/QueryBuilder/BaseQuery.cs b/QueryBuilder/BaseQuery.cs index 86b44a23..2b82891c 100644 --- a/QueryBuilder/BaseQuery.cs +++ b/QueryBuilder/BaseQuery.cs @@ -16,6 +16,8 @@ public abstract partial class BaseQuery : AbstractQuery where Q : BaseQuery parts, string columnName, object[] value) + { + StringBuilder casewrap = new StringBuilder($"{Wrap(columnName)} = "); + bool hasOne = false; + + foreach (var item in value) + { + if (item is object[] i && i.Length >= 3) + { + int indent = 0; + + object val = i.Last(); + var subparts = i.Take(i.Length - 1).ToArray(); + + int pointer = 0; + bool substart = true; + bool start = true; + bool setasfield = false; + bool criteriaValue = false; + var field = string.Empty; + + while (pointer <= (subparts.Length - 1)) + { + var piece = subparts[pointer].ToString().ToUpperInvariant().Trim(); + if (pointer > 0 && !substart) + { + if (!VERB.SpecialChar.Any(s => s == piece) && criteriaValue) + { + pointer = subparts.Length; + break; + } + else if (VERB.AndOpertors.Any(s => s == piece)) + { + casewrap.Append(" ").Append(VERB.And).Append(" "); + pointer++; + substart = true; + continue; + } + else if (VERB.OrOpertors.Any(s => s == piece)) + { + casewrap.Append(" ").Append(VERB.Or).Append(" "); + pointer++; + substart = true; + continue; + } + } + + if (!criteriaValue && VERB.AndOrOpertors.Any(s => s == piece)) + { + pointer = subparts.Length; + break; + } + else if (piece == VERB.StartParenth) + { + indent++; + pointer++; casewrap.Append(VERB.StartParenth); + continue; + } + else if (piece == VERB.EndParenth) + { + if (indent > 0) + { + indent--; + casewrap.Append(VERB.EndParenth); + pointer++; + continue; + } + } + + if (substart && !string.IsNullOrEmpty(field)) + { + criteriaValue = true; + } + if (piece == VERB.PushField && criteriaValue) + { + setasfield = true; + pointer++; + continue; + } + + else if (string.IsNullOrEmpty(field)) + { + field = piece; + } + if (substart && criteriaValue && !string.IsNullOrEmpty(field)) + { + + if (!hasOne && start) + { + casewrap.Append(VERB.CaseWhen); + hasOne = true; + } + else if (start) + { + casewrap.Append(" ").Append(VERB.When); + } + + casewrap.Append($" {field} = {(setasfield ? subparts[pointer] : Parameter(ctx, subparts[pointer]))}"); + substart = false; + setasfield = false; + start = false; + criteriaValue = false; + field = string.Empty; + } + pointer++; + + } + if (indent > 0 && hasOne) + { + casewrap.Append("".PadLeft(indent, ')')); + } + + if (hasOne) + { + casewrap.Append($" {VERB.Then} {Parameter(ctx, val)}"); + } + } + } + if (!hasOne) + { + casewrap.Append($"{Wrap(columnName)}"); + } + else + { + casewrap.Append($" {VERB.Else} {Wrap(columnName)} {VERB.End}"); + } + parts.Add(casewrap.ToString()); + casewrap.Length = 0; + } + + private void SetUpDirectUpdatePart(SqlResult ctx, List parts, string columnName, object value) + { + parts.Add($"{Wrap(columnName)} = {Parameter(ctx, value)}"); + } + protected virtual SqlResult CompileInsertQuery(Query query) { var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter) @@ -1021,6 +1165,10 @@ public virtual string Parameter(SqlResult ctx, object parameter) ctx.Bindings.Add(value); return parameterPlaceholder; } + else if (parameter == DBNull.Value) + { + parameter = null; + } ctx.Bindings.Add(parameter); return parameterPlaceholder; diff --git a/QueryBuilder/Constants.cs b/QueryBuilder/Constants.cs new file mode 100644 index 00000000..200572dc --- /dev/null +++ b/QueryBuilder/Constants.cs @@ -0,0 +1,27 @@ +namespace SqlKata +{ + public static class VERB + { + public const string PushField = "*"; + public const string StartParenth = "("; + public const string EndParenth = ")"; + public const string And = "AND"; + public const string And2 = "&&"; + public const string And3 = "&"; + public const string Or = "OR"; + public const string Or2 = "||"; + public const string Or3 = "|"; + public const string Or4 = "<>"; + public const string CaseWhen = "CASE WHEN"; + public const string When = "WHEN"; + public const string Else = "ELSE"; + public const string Then = "THEN"; + public const string End = "END"; + + + public static string[] SpecialChar = [StartParenth, EndParenth, PushField]; + public static string[] AndOrOpertors = [And, And2, And3, Or, Or2, Or3, Or4]; + public static string[] AndOpertors = [And, And2, And3]; + public static string[] OrOpertors = [Or, Or2, Or3, Or4]; + } +} diff --git a/SqlKata.Execution/QueryFactory.cs b/SqlKata.Execution/QueryFactory.cs index 5ff399aa..ba1fa57a 100644 --- a/SqlKata.Execution/QueryFactory.cs +++ b/SqlKata.Execution/QueryFactory.cs @@ -859,9 +859,8 @@ private static async Task> handleIncludesAsync(Query query, IE internal SqlResult CompileAndLog(Query query) { var compiled = this.Compiler.Compile(query); - + query.ToRaw = compiled.RawSql; this.Logger(compiled); - return compiled; } From 96ebb57727fba8628f29d204b3701d9f951a0330 Mon Sep 17 00:00:00 2001 From: Thomas Franey Date: Tue, 5 Aug 2025 16:05:21 -0500 Subject: [PATCH 2/3] fix null comparison in case when --- QueryBuilder/Compilers/Compiler.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/QueryBuilder/Compilers/Compiler.cs b/QueryBuilder/Compilers/Compiler.cs index 4f8d0732..abd0609c 100644 --- a/QueryBuilder/Compilers/Compiler.cs +++ b/QueryBuilder/Compilers/Compiler.cs @@ -420,8 +420,8 @@ private void SetUpCaseWhenUpdatePart(SqlResult ctx, List parts, string c while (pointer <= (subparts.Length - 1)) { - var piece = subparts[pointer].ToString().ToUpperInvariant().Trim(); - if (pointer > 0 && !substart) + var piece = subparts[pointer]?.ToString().ToUpperInvariant().Trim(); + if (pointer > 0 && !substart && piece != null) { if (!VERB.SpecialChar.Any(s => s == piece) && criteriaValue) { @@ -444,7 +444,8 @@ private void SetUpCaseWhenUpdatePart(SqlResult ctx, List parts, string c } } - if (!criteriaValue && VERB.AndOrOpertors.Any(s => s == piece)) + + if (!criteriaValue && (piece is null || VERB.AndOrOpertors.Any(s => s == piece))) { pointer = subparts.Length; break; @@ -481,6 +482,7 @@ private void SetUpCaseWhenUpdatePart(SqlResult ctx, List parts, string c { field = piece; } + if (substart && criteriaValue && !string.IsNullOrEmpty(field)) { @@ -494,7 +496,14 @@ private void SetUpCaseWhenUpdatePart(SqlResult ctx, List parts, string c casewrap.Append(" ").Append(VERB.When); } - casewrap.Append($" {field} = {(setasfield ? subparts[pointer] : Parameter(ctx, subparts[pointer]))}"); + if (subparts[pointer] is not null) + { + casewrap.Append($" {field} = {(setasfield ? subparts[pointer] : Parameter(ctx, subparts[pointer]))}"); + } + else + { + casewrap.Append($" {field} is NULL"); + } substart = false; setasfield = false; start = false; From d893646fb459becb8de532063332b98a2ed35771 Mon Sep 17 00:00:00 2001 From: Thomas Franey Date: Tue, 5 Aug 2025 23:15:55 -0500 Subject: [PATCH 3/3] fix readme --- NewReadMe.md | 84 ++++++++++++++++++++++++++-------------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/NewReadMe.md b/NewReadMe.md index 2146d3bf..58296416 100644 --- a/NewReadMe.md +++ b/NewReadMe.md @@ -1,41 +1,41 @@ -* New Update and UpdateAsync upgrade: CASE +# New Update and UpdateAsync upgrade: CASE -** A new feature added to allow developers to programmatically set CASE WHEN when assigning values. Feature includes grouping in sub statements () or -** to allow condition to point to a column variable instead of a direct paramater value. SQL injection friendly +## A new feature added to allow developers to programmatically set CASE WHEN when assigning values. Feature includes grouping in sub statements () or +## to allow condition to point to a column variable instead of a direct paramater value. SQL injection friendly -** Original Update Statement for multiple records using anonymous objects: +## Original Update Statement for multiple records using anonymous objects: -*** foreach (var item in data) +### foreach (var item in data) -*** { +### { -*** object obj = new +### object obj = new -*** { +### { -*** MyField = item.Value +### MyField = item.Value -*** }; +### }; -*** cnt += await QueryFactory.Query(tableName).Where("Id", item.Id).UpdateAsync(value); +### cnt += await QueryFactory.Query(tableName).Where("Id", item.Id).UpdateAsync(value); -*** } +### } -*** return cnt; +### return cnt; -** New Update with select case using multi-level array systems -** version 1 : allows is equal condition only for now -** For the Else it will always fill with name of field itself , self assigning. -** This happens if format is wrong as well. -** The else protects you fro your field to be set back to NULL +## New Update with select case using multi-level array systems +## version 1 : allows is equal condition only for now +## For the Else it will always fill with name of field itself , self assigning. +## This happens if format is wrong as well. +## The else protects you fro your field to be set back to NULL -*** Warning: Limitation is requires , Suggest 200 rows for low number columns, -*** 25 for higher number columns or clauses. +### Warning: Limitation is requires , Suggest 200 rows for low number columns, +### 25 for higher number columns or clauses. var datac = data.Chunk(200); // breaking data up to 200 rows @@ -73,40 +73,40 @@ -**standard: Case WHEN x = A then Y... END: -*** In your cases array the flow is [x,A,Y]. -*** Assignmet value is always last. +##standard: Case WHEN x = A then Y... END: +### In your cases array the flow is [x,A,Y]. +### Assignmet value is always last. -** Available Feaure 1 : While its common to do 3 items for basic, when can extend the criteria with AND and OR -** It combine, the array column after the orevioud criteria field must be an AND or OR, unless using , () or * explained later +## Available Feaure 1 : While its common to do 3 items for basic, when can extend the criteria with AND and OR +## It combine, the array column after the orevioud criteria field must be an AND or OR, unless using , () or * explained later -*** Note: Assignmet value is always last. you can use AND,&&,& or OR,||,|, <>. Not case sensitive. +### Note: Assignmet value is always last. you can use AND,&&,& or OR,||,|, <>. Not case sensitive. -*** Case WHEN x = A AND z = B then Y ... END: -*** In your cases array the flow is [x,A,"AND",z,B,Y] -*** Case WHEN x = A OR z = B then Y ... END: -*** Array the flow is [x,A,"OR",z,B,Y] +### Case WHEN x = A AND z = B then Y ... END: +### In your cases array the flow is [x,A,"AND",z,B,Y] +### Case WHEN x = A OR z = B then Y ... END: +### Array the flow is [x,A,"OR",z,B,Y] -** Available Feaure 2 : Subset (). This allows seperating your "And" & "Or" blocks -*** ex: case when (a = 1 or a = 5) and (b = 7 and c = 2) -*** This can be placed anywhere before the assignment column or * assignment column, -*** if you forget to add the ) to close, the engine -*** will compensate. +## Available Feaure 2 : Subset (). This allows seperating your "And" & "Or" blocks +### ex: case when (a = 1 or a = 5) and (b = 7 and c = 2) +### This can be placed anywhere before the assignment column or * assignment column, +### if you forget to add the ) to close, the engine +### will compensate. -*** Case WHEN (x = A AND z = B) OR J = C then Y ... END: -*** Array the flow is ["(",x,A,"AND",z,B,")","OR",j,c,Y] -*** Case WHEN (x = A OR z = B) AND (J = C AND K = D) then Y ... END: -*** Array the flow is ["(",x,A,"OR",z,B,")","AND","(",j,c,"AND",k,d,")" Y] +### Case WHEN (x = A AND z = B) OR J = C then Y ... END: +### Array the flow is ["(",x,A,"AND",z,B,")","OR",j,c,Y] +### Case WHEN (x = A OR z = B) AND (J = C AND K = D) then Y ... END: +### Array the flow is ["(",x,A,"OR",z,B,")","AND","(",j,c,"AND",k,d,")" Y] -** Available Feaure 3 : To Another Column Field (*). This allows criteria to check if column equals another column (field) -*** Case WHEN (colx = colb AND colz = colx) then Y ... END: -*** Array the flow is [,colx,*',colb,"AND",colz,colx, Y] +## Available Feaure 3 : To Another Column Field (*). This allows criteria to check if column equals another column (field) +### Case WHEN (colx = colb AND colz = colx) then Y ... END: +### Array the flow is [,colx,*',colb,"AND",colz,colx, Y]