Skip to content

Commit 8d5e7d2

Browse files
authored
Merge branch 'sqlkata:master' into NullableInclude
2 parents 2593550 + 87b4947 commit 8d5e7d2

File tree

7 files changed

+206
-1
lines changed

7 files changed

+206
-1
lines changed

QueryBuilder.Tests/GeneralTests.cs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,106 @@ public void Where_Nested()
402402
Assert.Equal("SELECT * FROM [table] WHERE ([a] = 1 OR [a] = 2)", c[EngineCodes.SqlServer].ToString());
403403
}
404404

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 [a] FROM (VALUES (1)) AS tbl ([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 [a], [b], [c] FROM (VALUES (1, 2, 3), (4, 5, 6)) AS tbl ([a], [b], [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+
}
481+
482+
[Fact]
483+
public void AdHoc_ProperBindingsPlacement()
484+
{
485+
var query = new Query("rows")
486+
.With("othercte", q => q.From("othertable").Where("othertable.status", "A"))
487+
.Where("rows.foo", "bar")
488+
.With("rows",
489+
new[] { "a", "b", "c" },
490+
new object[][] {
491+
new object[] { 1, 2, 3 },
492+
new object[] { 4, 5, 6 },
493+
})
494+
.Where("rows.baz", "buzz");
495+
496+
var c = Compilers.Compile(query);
497+
498+
Assert.Equal(string.Join("\n", new[] {
499+
"WITH [othercte] AS (SELECT * FROM [othertable] WHERE [othertable].[status] = 'A'),",
500+
"[rows] AS (SELECT [a], [b], [c] FROM (VALUES (1, 2, 3), (4, 5, 6)) AS tbl ([a], [b], [c]))",
501+
"SELECT * FROM [rows] WHERE [rows].[foo] = 'bar' AND [rows].[baz] = 'buzz'",
502+
}), c[EngineCodes.SqlServer].ToString());
503+
}
504+
405505
[Fact]
406506
public void UnsafeLiteral_Insert()
407507
{

QueryBuilder/Clauses/FromClause.cs

Lines changed: 23 additions & 1 deletion
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
}
97-
}
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+
}
119+
}

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 {Wrap(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
protected virtual SqlResult CompileDeleteQuery(Query query)
213235
{
214236
var ctx = new SqlResult
@@ -498,6 +520,13 @@ public virtual SqlResult CompileCte(AbstractFrom cte)
498520

499521
ctx.RawSql = $"{WrapValue(queryFromClause.Alias)} AS ({subCtx.RawSql})";
500522
}
523+
else if (cte is AdHocTableFromClause adHoc)
524+
{
525+
var subCtx = CompileAdHocQuery(adHoc);
526+
ctx.Bindings.AddRange(subCtx.Bindings);
527+
528+
ctx.RawSql = $"{WrapValue(adHoc.Alias)} AS ({subCtx.RawSql})";
529+
}
501530

502531
return ctx;
503532
}

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/Compilers/SqlServerCompiler.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Linq;
2+
13
namespace SqlKata.Compilers
24
{
35
public class SqlServerCompiler : Compiler
@@ -168,5 +170,21 @@ protected override string CompileBasicDateCondition(SqlResult ctx, BasicDateCond
168170

169171
return sql;
170172
}
173+
174+
protected override SqlResult CompileAdHocQuery(AdHocTableFromClause adHoc)
175+
{
176+
var ctx = new SqlResult();
177+
178+
var colNames = string.Join(", ", adHoc.Columns.Select(Wrap));
179+
180+
var valueRow = string.Join(", ", Enumerable.Repeat("?", adHoc.Columns.Count));
181+
var valueRows = string.Join(", ", Enumerable.Repeat($"({valueRow})", adHoc.Values.Count / adHoc.Columns.Count));
182+
var sql = $"SELECT {colNames} FROM (VALUES {valueRows}) AS tbl ({colNames})";
183+
184+
ctx.RawSql = sql;
185+
ctx.Bindings = adHoc.Values;
186+
187+
return ctx;
188+
}
171189
}
172190
}

QueryBuilder/Query.cs

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

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

0 commit comments

Comments
 (0)