Skip to content

Commit c832c52

Browse files
Add ad-hoc default implementation
This uses the UNION ALL syntax for all compilers
1 parent c860a3d commit c832c52

6 files changed

Lines changed: 164 additions & 0 deletions

File tree

QueryBuilder.Tests/GeneralTests.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,5 +401,82 @@ public void Where_Nested()
401401

402402
Assert.Equal("SELECT * FROM [table] WHERE ([a] = 1 OR [a] = 2)", c[EngineCodes.SqlServer].ToString());
403403
}
404+
405+
[Fact]
406+
public void AdHoc_Throws_WhenNoColumnsProvided() =>
407+
Assert.Throws<InvalidOperationException>(() =>
408+
new Query("rows").With("rows",
409+
new string[0],
410+
new object[][] {
411+
new object[] {},
412+
new object[] {},
413+
}));
414+
415+
[Fact]
416+
public void AdHoc_Throws_WhenNoValueRowsProvided() =>
417+
Assert.Throws<InvalidOperationException>(() =>
418+
new Query("rows").With("rows",
419+
new[] { "a", "b", "c" },
420+
new object[][] {
421+
}));
422+
423+
[Fact]
424+
public void AdHoc_Throws_WhenColumnsOutnumberFieldValues() =>
425+
Assert.Throws<InvalidOperationException>(() =>
426+
new Query("rows").With("rows",
427+
new[] { "a", "b", "c", "d" },
428+
new object[][] {
429+
new object[] { 1, 2, 3 },
430+
new object[] { 4, 5, 6 },
431+
}));
432+
433+
[Fact]
434+
public void AdHoc_Throws_WhenFieldValuesOutNumberColumns() =>
435+
Assert.Throws<InvalidOperationException>(() =>
436+
new Query("rows").With("rows",
437+
new[] { "a", "b" },
438+
new object[][] {
439+
new object[] { 1, 2, 3 },
440+
new object[] { 4, 5, 6 },
441+
}));
442+
443+
[Fact]
444+
public void AdHoc_SingletonRow()
445+
{
446+
var query = new Query("rows").With("rows",
447+
new[] { "a" },
448+
new object[][] {
449+
new object[] { 1 },
450+
});
451+
452+
var c = Compilers.Compile(query);
453+
454+
Assert.Equal("WITH [rows] AS (SELECT 1 AS a)\nSELECT * FROM [rows]", c[EngineCodes.SqlServer].ToString());
455+
Assert.Equal("WITH \"rows\" AS (SELECT 1 AS a)\nSELECT * FROM \"rows\"", c[EngineCodes.PostgreSql].ToString());
456+
Assert.Equal("WITH `rows` AS (SELECT 1 AS a)\nSELECT * FROM `rows`", c[EngineCodes.MySql].ToString());
457+
Assert.Equal("WITH \"rows\" AS (SELECT 1 AS a)\nSELECT * FROM \"rows\"", c[EngineCodes.Sqlite].ToString());
458+
Assert.Equal("WITH \"ROWS\" AS (SELECT 1 AS a FROM RDB$DATABASE)\nSELECT * FROM \"ROWS\"", c[EngineCodes.Firebird].ToString());
459+
Assert.Equal("WITH \"rows\" AS (SELECT 1 AS a FROM DUAL)\nSELECT * FROM \"rows\"", c[EngineCodes.Oracle].ToString());
460+
}
461+
462+
[Fact]
463+
public void AdHoc_TwoRows()
464+
{
465+
var query = new Query("rows").With("rows",
466+
new[] { "a", "b", "c" },
467+
new object[][] {
468+
new object[] { 1, 2, 3 },
469+
new object[] { 4, 5, 6 },
470+
});
471+
472+
var c = Compilers.Compile(query);
473+
474+
Assert.Equal("WITH [rows] AS (SELECT 1 AS a, 2 AS b, 3 AS c UNION ALL SELECT 4 AS a, 5 AS b, 6 AS c)\nSELECT * FROM [rows]", c[EngineCodes.SqlServer].ToString());
475+
Assert.Equal("WITH \"rows\" AS (SELECT 1 AS a, 2 AS b, 3 AS c UNION ALL SELECT 4 AS a, 5 AS b, 6 AS c)\nSELECT * FROM \"rows\"", c[EngineCodes.PostgreSql].ToString());
476+
Assert.Equal("WITH `rows` AS (SELECT 1 AS a, 2 AS b, 3 AS c UNION ALL SELECT 4 AS a, 5 AS b, 6 AS c)\nSELECT * FROM `rows`", c[EngineCodes.MySql].ToString());
477+
Assert.Equal("WITH \"rows\" AS (SELECT 1 AS a, 2 AS b, 3 AS c UNION ALL SELECT 4 AS a, 5 AS b, 6 AS c)\nSELECT * FROM \"rows\"", c[EngineCodes.Sqlite].ToString());
478+
Assert.Equal("WITH \"ROWS\" AS (SELECT 1 AS a, 2 AS b, 3 AS c FROM RDB$DATABASE UNION ALL SELECT 4 AS a, 5 AS b, 6 AS c FROM RDB$DATABASE)\nSELECT * FROM \"ROWS\"", c[EngineCodes.Firebird].ToString());
479+
Assert.Equal("WITH \"rows\" AS (SELECT 1 AS a, 2 AS b, 3 AS c FROM DUAL UNION ALL SELECT 4 AS a, 5 AS b, 6 AS c FROM DUAL)\nSELECT * FROM \"rows\"", c[EngineCodes.Oracle].ToString());
480+
}
404481
}
405482
}

QueryBuilder/Clauses/FromClause.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23

34
namespace SqlKata
45
{
@@ -94,4 +95,25 @@ public override AbstractClause Clone()
9495
};
9596
}
9697
}
98+
99+
/// <summary>
100+
/// Represents a FROM clause that is an ad-hoc table built with predefined values.
101+
/// </summary>
102+
public class AdHocTableFromClause : AbstractFrom
103+
{
104+
public List<string> Columns { get; set; }
105+
public List<object> Values { get; set; }
106+
107+
public override AbstractClause Clone()
108+
{
109+
return new AdHocTableFromClause
110+
{
111+
Engine = Engine,
112+
Alias = Alias,
113+
Columns = Columns,
114+
Values = Values,
115+
Component = Component,
116+
};
117+
}
118+
}
97119
}

QueryBuilder/Compilers/Compiler.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ protected Compiler()
2424

2525
public virtual string EngineCode { get; }
2626

27+
protected virtual string SingleRowDummyTableName { get => null; }
2728

2829
/// <summary>
2930
/// A list of white-listed operators
@@ -209,6 +210,27 @@ protected virtual SqlResult CompileSelectQuery(Query query)
209210
return ctx;
210211
}
211212

213+
protected virtual SqlResult CompileAdHocQuery(AdHocTableFromClause adHoc)
214+
{
215+
var ctx = new SqlResult();
216+
217+
var row = "SELECT " + string.Join(", ", adHoc.Columns.Select(col => $"? AS {WrapIdentifiers(col)}"));
218+
219+
var fromTable = SingleRowDummyTableName;
220+
221+
if (fromTable != null)
222+
{
223+
row += $" FROM {fromTable}";
224+
}
225+
226+
var rows = string.Join(" UNION ALL ", Enumerable.Repeat(row, adHoc.Values.Count / adHoc.Columns.Count));
227+
228+
ctx.RawSql = rows;
229+
ctx.Bindings = adHoc.Values;
230+
231+
return ctx;
232+
}
233+
212234
private SqlResult CompileDeleteQuery(Query query)
213235
{
214236
var ctx = new SqlResult
@@ -477,6 +499,13 @@ public virtual SqlResult CompileCte(AbstractFrom cte)
477499

478500
ctx.RawSql = $"{WrapValue(queryFromClause.Alias)} AS ({subCtx.RawSql})";
479501
}
502+
else if (cte is AdHocTableFromClause adHoc)
503+
{
504+
var subCtx = CompileAdHocQuery(adHoc);
505+
ctx.Bindings.AddRange(subCtx.Bindings);
506+
507+
ctx.RawSql = $"{WrapValue(adHoc.Alias)} AS ({subCtx.RawSql})";
508+
}
480509

481510
return ctx;
482511
}

QueryBuilder/Compilers/FirebirdCompiler.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public FirebirdCompiler()
1111
}
1212

1313
public override string EngineCode { get; } = EngineCodes.Firebird;
14+
protected override string SingleRowDummyTableName => "RDB$DATABASE";
1415

1516
protected override SqlResult CompileInsertQuery(Query query)
1617
{

QueryBuilder/Compilers/OracleCompiler.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public OracleCompiler()
1616

1717
public override string EngineCode { get; } = EngineCodes.Oracle;
1818
public bool UseLegacyPagination { get; set; } = false;
19+
protected override string SingleRowDummyTableName => "DUAL";
1920

2021
protected override SqlResult CompileSelectQuery(Query query)
2122
{

QueryBuilder/Query.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,40 @@ public Query With(string alias, Func<Query, Query> fn)
118118
return With(alias, fn.Invoke(new Query()));
119119
}
120120

121+
/// <summary>
122+
/// Constructs an ad-hoc table of the given data as a CTE.
123+
/// </summary>
124+
public Query With(string alias, IEnumerable<string> columns, IEnumerable<IEnumerable<object>> valuesCollection)
125+
{
126+
var columnsList = columns?.ToList();
127+
var valuesCollectionList = valuesCollection?.ToList();
128+
129+
if ((columnsList?.Count ?? 0) == 0 || (valuesCollectionList?.Count ?? 0) == 0)
130+
{
131+
throw new InvalidOperationException("Columns and valuesCollection cannot be null or empty");
132+
}
133+
134+
var clause = new AdHocTableFromClause()
135+
{
136+
Alias = alias,
137+
Columns = columnsList,
138+
Values = new List<object>(),
139+
};
140+
141+
foreach (var values in valuesCollectionList)
142+
{
143+
var valuesList = values.ToList();
144+
if (columnsList.Count != valuesList.Count)
145+
{
146+
throw new InvalidOperationException("Columns count should be equal to each Values count");
147+
}
148+
149+
clause.Values.AddRange(valuesList);
150+
}
151+
152+
return AddComponent("cte", clause);
153+
}
154+
121155
public Query WithRaw(string alias, string sql, params object[] bindings)
122156
{
123157
return AddComponent("cte", new RawFromClause

0 commit comments

Comments
 (0)