Skip to content

Commit 2f75345

Browse files
committed
Issue-757: added Query update with CASE WHEN capabilities
1 parent d20e930 commit 2f75345

File tree

5 files changed

+291
-3
lines changed

5 files changed

+291
-3
lines changed

NewReadMe.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
* New Update and UpdateAsync upgrade: CASE
2+
3+
** A new feature added to allow developers to programmatically set CASE WHEN when assigning values. Feature includes grouping in sub statements () or
4+
** to allow condition to point to a column variable instead of a direct paramater value. SQL injection friendly
5+
6+
** Original Update Statement for multiple records using anonymous objects:
7+
8+
*** foreach (var item in data)
9+
10+
*** {
11+
12+
*** object obj = new
13+
14+
*** {
15+
16+
*** MyField = item.Value
17+
18+
*** };
19+
20+
*** cnt += await QueryFactory.Query(tableName).Where("Id", item.Id).UpdateAsync(value);
21+
22+
23+
*** }
24+
25+
*** return cnt;
26+
27+
28+
29+
30+
31+
** New Update with select case using multi-level array systems
32+
** version 1 : allows is equal condition only for now
33+
** For the Else it will always fill with name of field itself , self assigning.
34+
** This happens if format is wrong as well.
35+
** The else protects you fro your field to be set back to NULL
36+
37+
*** Warning: Limitation is requires , Suggest 200 rows for low number columns,
38+
*** 25 for higher number columns or clauses.
39+
40+
41+
var datac = data.Chunk(200); // breaking data up to 200 rows
42+
43+
//each holds for each update set, which allows multiple value setting as older Update
44+
List<object[]> cases = [];
45+
46+
if (datac.Any()) foreach (var d in datac)
47+
{
48+
49+
try
50+
{
51+
foreach (var item in d) //Build case when statement , standard 3
52+
{
53+
cases.Add(["Id", item.Id, item.Value]);
54+
}
55+
object obj = new
56+
{
57+
MyField= cases.ToArray()
58+
};
59+
cases.Clear();
60+
61+
//if data set is smaller than whole table , best to use in statement to reduce cost
62+
cnt += await QueryFactory.Query(tableName)
63+
.WhereIn("Id", d.Select(dd => dd.Id).ToArray())
64+
.UpdateAsync(value);
65+
}
66+
catch { throw; }
67+
finally { cases.Clear(); }
68+
}
69+
else cases.Clear();
70+
71+
return cnt;
72+
73+
74+
75+
76+
**standard: Case WHEN x = A then Y... END:
77+
*** In your cases array the flow is [x,A,Y].
78+
*** Assignmet value is always last.
79+
80+
81+
82+
83+
84+
** Available Feaure 1 : While its common to do 3 items for basic, when can extend the criteria with AND and OR
85+
** It combine, the array column after the orevioud criteria field must be an AND or OR, unless using , () or * explained later
86+
87+
*** Note: Assignmet value is always last. you can use AND,&&,& or OR,||,|, <>. Not case sensitive.
88+
89+
*** Case WHEN x = A AND z = B then Y ... END:
90+
*** In your cases array the flow is [x,A,"AND",z,B,Y]
91+
*** Case WHEN x = A OR z = B then Y ... END:
92+
*** Array the flow is [x,A,"OR",z,B,Y]
93+
94+
95+
96+
97+
98+
** Available Feaure 2 : Subset (). This allows seperating your "And" & "Or" blocks
99+
*** ex: case when (a = 1 or a = 5) and (b = 7 and c = 2)
100+
*** This can be placed anywhere before the assignment column or * assignment column,
101+
*** if you forget to add the ) to close, the engine
102+
*** will compensate.
103+
104+
*** Case WHEN (x = A AND z = B) OR J = C then Y ... END:
105+
*** Array the flow is ["(",x,A,"AND",z,B,")","OR",j,c,Y]
106+
*** Case WHEN (x = A OR z = B) AND (J = C AND K = D) then Y ... END:
107+
*** Array the flow is ["(",x,A,"OR",z,B,")","AND","(",j,c,"AND",k,d,")" Y]
108+
109+
110+
** Available Feaure 3 : To Another Column Field (*). This allows criteria to check if column equals another column (field)
111+
*** Case WHEN (colx = colb AND colz = colx) then Y ... END:
112+
*** Array the flow is [,colx,*',colb,"AND",colz,colx, Y]

QueryBuilder/BaseQuery.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public abstract partial class BaseQuery<Q> : AbstractQuery where Q : BaseQuery<Q
1616
private bool orFlag = false;
1717
private bool notFlag = false;
1818
public string EngineScope = null;
19+
public string ToRaw { get; set; }
20+
1921

2022
public Q SetEngineScope(string engine)
2123
{

QueryBuilder/Compilers/Compiler.cs

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,16 @@ protected virtual SqlResult CompileUpdateQuery(Query query)
371371

372372
for (var i = 0; i < toUpdate.Columns.Count; i++)
373373
{
374-
parts.Add(Wrap(toUpdate.Columns[i]) + " = " + Parameter(ctx, toUpdate.Values[i]));
374+
var values = toUpdate.Values[i];
375+
if (values is object[] v && v.Length > 0)
376+
{
377+
SetUpCaseWhenUpdatePart(ctx, parts, toUpdate.Columns[i], v);
378+
}
379+
else if (values is not Array)
380+
{
381+
SetUpDirectUpdatePart(ctx, parts, toUpdate.Columns[i], values);
382+
}
383+
else throw new MissingMemberException();
375384
}
376385

377386
var sets = string.Join(", ", parts);
@@ -388,6 +397,141 @@ protected virtual SqlResult CompileUpdateQuery(Query query)
388397
return ctx;
389398
}
390399

400+
private void SetUpCaseWhenUpdatePart(SqlResult ctx, List<string> parts, string columnName, object[] value)
401+
{
402+
StringBuilder casewrap = new StringBuilder($"{Wrap(columnName)} = ");
403+
bool hasOne = false;
404+
405+
foreach (var item in value)
406+
{
407+
if (item is object[] i && i.Length >= 3)
408+
{
409+
int indent = 0;
410+
411+
object val = i.Last();
412+
var subparts = i.Take(i.Length - 1).ToArray();
413+
414+
int pointer = 0;
415+
bool substart = true;
416+
bool start = true;
417+
bool setasfield = false;
418+
bool criteriaValue = false;
419+
var field = string.Empty;
420+
421+
while (pointer <= (subparts.Length - 1))
422+
{
423+
var piece = subparts[pointer].ToString().ToUpperInvariant().Trim();
424+
if (pointer > 0 && !substart)
425+
{
426+
if (!VERB.SpecialChar.Any(s => s == piece) && criteriaValue)
427+
{
428+
pointer = subparts.Length;
429+
break;
430+
}
431+
else if (VERB.AndOpertors.Any(s => s == piece))
432+
{
433+
casewrap.Append(" ").Append(VERB.And).Append(" ");
434+
pointer++;
435+
substart = true;
436+
continue;
437+
}
438+
else if (VERB.OrOpertors.Any(s => s == piece))
439+
{
440+
casewrap.Append(" ").Append(VERB.Or).Append(" ");
441+
pointer++;
442+
substart = true;
443+
continue;
444+
}
445+
}
446+
447+
if (!criteriaValue && VERB.AndOrOpertors.Any(s => s == piece))
448+
{
449+
pointer = subparts.Length;
450+
break;
451+
}
452+
else if (piece == VERB.StartParenth)
453+
{
454+
indent++;
455+
pointer++; casewrap.Append(VERB.StartParenth);
456+
continue;
457+
}
458+
else if (piece == VERB.EndParenth)
459+
{
460+
if (indent > 0)
461+
{
462+
indent--;
463+
casewrap.Append(VERB.EndParenth);
464+
pointer++;
465+
continue;
466+
}
467+
}
468+
469+
if (substart && !string.IsNullOrEmpty(field))
470+
{
471+
criteriaValue = true;
472+
}
473+
if (piece == VERB.PushField && criteriaValue)
474+
{
475+
setasfield = true;
476+
pointer++;
477+
continue;
478+
}
479+
480+
else if (string.IsNullOrEmpty(field))
481+
{
482+
field = piece;
483+
}
484+
if (substart && criteriaValue && !string.IsNullOrEmpty(field))
485+
{
486+
487+
if (!hasOne && start)
488+
{
489+
casewrap.Append(VERB.CaseWhen);
490+
hasOne = true;
491+
}
492+
else if (start)
493+
{
494+
casewrap.Append(" ").Append(VERB.When);
495+
}
496+
497+
casewrap.Append($" {field} = {(setasfield ? subparts[pointer] : Parameter(ctx, subparts[pointer]))}");
498+
substart = false;
499+
setasfield = false;
500+
start = false;
501+
criteriaValue = false;
502+
field = string.Empty;
503+
}
504+
pointer++;
505+
506+
}
507+
if (indent > 0 && hasOne)
508+
{
509+
casewrap.Append("".PadLeft(indent, ')'));
510+
}
511+
512+
if (hasOne)
513+
{
514+
casewrap.Append($" {VERB.Then} {Parameter(ctx, val)}");
515+
}
516+
}
517+
}
518+
if (!hasOne)
519+
{
520+
casewrap.Append($"{Wrap(columnName)}");
521+
}
522+
else
523+
{
524+
casewrap.Append($" {VERB.Else} {Wrap(columnName)} {VERB.End}");
525+
}
526+
parts.Add(casewrap.ToString());
527+
casewrap.Length = 0;
528+
}
529+
530+
private void SetUpDirectUpdatePart(SqlResult ctx, List<string> parts, string columnName, object value)
531+
{
532+
parts.Add($"{Wrap(columnName)} = {Parameter(ctx, value)}");
533+
}
534+
391535
protected virtual SqlResult CompileInsertQuery(Query query)
392536
{
393537
var ctx = new SqlResult(parameterPlaceholder, EscapeCharacter)
@@ -1021,6 +1165,10 @@ public virtual string Parameter(SqlResult ctx, object parameter)
10211165
ctx.Bindings.Add(value);
10221166
return parameterPlaceholder;
10231167
}
1168+
else if (parameter == DBNull.Value)
1169+
{
1170+
parameter = null;
1171+
}
10241172

10251173
ctx.Bindings.Add(parameter);
10261174
return parameterPlaceholder;

QueryBuilder/Constants.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace SqlKata
2+
{
3+
public static class VERB
4+
{
5+
public const string PushField = "*";
6+
public const string StartParenth = "(";
7+
public const string EndParenth = ")";
8+
public const string And = "AND";
9+
public const string And2 = "&&";
10+
public const string And3 = "&";
11+
public const string Or = "OR";
12+
public const string Or2 = "||";
13+
public const string Or3 = "|";
14+
public const string Or4 = "<>";
15+
public const string CaseWhen = "CASE WHEN";
16+
public const string When = "WHEN";
17+
public const string Else = "ELSE";
18+
public const string Then = "THEN";
19+
public const string End = "END";
20+
21+
22+
public static string[] SpecialChar = [StartParenth, EndParenth, PushField];
23+
public static string[] AndOrOpertors = [And, And2, And3, Or, Or2, Or3, Or4];
24+
public static string[] AndOpertors = [And, And2, And3];
25+
public static string[] OrOpertors = [Or, Or2, Or3, Or4];
26+
}
27+
}

SqlKata.Execution/QueryFactory.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -859,9 +859,8 @@ private static async Task<IEnumerable<T>> handleIncludesAsync<T>(Query query, IE
859859
internal SqlResult CompileAndLog(Query query)
860860
{
861861
var compiled = this.Compiler.Compile(query);
862-
862+
query.ToRaw = compiled.RawSql;
863863
this.Logger(compiled);
864-
865864
return compiled;
866865
}
867866

0 commit comments

Comments
 (0)