-
Notifications
You must be signed in to change notification settings - Fork 2
Common Table Expressions
Common Table Expressions (CTEs) let you create a subquery and refer to it later in the same statement. SQLite supports two forms, non-recursive (WITH) and recursive (WITH RECURSIVE).
Use With to define a CTE from any LINQ query:
SQLiteCte<Book> expensiveBooks = db.With(() =>
db.Table<Book>().Where(b => b.Price > 30));
List<Book> results = await (from b in expensiveBooks select b).ToListAsync();Generated SQL:
WITH cte0 AS (
SELECT b1."BookId" AS "Id",
b1."BookTitle" AS "Title",
b1."BookAuthorId" AS "AuthorId",
b1."BookPrice" AS "Price"
FROM "Books" AS b1
WHERE b1."BookPrice" > @p0
)
SELECT b0."Id" AS "Id",
b0."Title" AS "Title",
b0."AuthorId" AS "AuthorId",
b0."Price" AS "Price"
FROM cte0 AS b0The CTE body can be any queryable expression including Where, Select, OrderBy, joins and set operations.
SQLite 3.35 added MATERIALIZED and NOT MATERIALIZED hints that force or block CTE inlining. Pass a SQLiteCteMaterialization value to With or WithRecursive to set the hint. The default emits no hint and lets SQLite choose.
SQLiteCte<Book> expensive = db.With(
() => db.Table<Book>().Where(b => b.Price > 100),
SQLiteCteMaterialization.Materialized);Materialized forces SQLite to compute the CTE body once and reuse the result. NotMaterialized forces SQLite to inline the body at every reference.
A CTE can be used on either side of a join:
SQLiteCte<Book> cheapBooks = db.With(() =>
db.Table<Book>().Where(b => b.Price < 20));
var results = await (
from b in cheapBooks
join a in db.Table<Author>() on b.AuthorId equals a.Id
select new { b.Title, AuthorName = a.Name }
).ToListAsync();Values wraps a single row of data into a queryable, useful as the seed row in a recursive CTE:
IQueryable<Seed> seed = db.Values(new Seed { X = 0, Label = "start" });This generates (SELECT @p0 AS "X", @p1 AS "Label") AS s0 as the FROM clause.
Use ValuesRange to lift an in-memory list into a query without a temporary table. Each item becomes one row, so you can join or filter against it. An empty list yields no rows.
var ids = new[] { 1, 2, 3 };
var titles = await (
from id in db.ValuesRange(ids)
join book in db.Table<Book>() on id equals book.Id
select book.Title).ToListAsync();Use WithRecursive when the query needs to reference itself. The lambda parameter is the self-reference.
The simplest recursive CTE counts from 1 to 10:
SQLiteCte<Cnt> counter = db.WithRecursive<Cnt>(self =>
db.Values(new Cnt { X = 1 })
.Concat(from c in self where c.X < 10 select new Cnt { X = c.X + 1 }));
List<Cnt> rows = await (from c in counter select c).ToListAsync();
// rows = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]Generated SQL:
WITH RECURSIVE cte0 AS (
SELECT c1."X" AS "X"
FROM (SELECT column1 AS "X" FROM (VALUES (@p1))) AS c1
UNION ALL
SELECT (c2."X" + @p3) AS "X"
FROM cte0 AS c2
WHERE c2."X" < @p2
)
SELECT c0."X" AS "X"
FROM cte0 AS c0
ORDER BY c0."X" ASCGenerate Fibonacci numbers up to 100:
class Fib { public int A { get; set; } public int B { get; set; } }
SQLiteCte<Fib> fib = db.WithRecursive<Fib>(self =>
db.Values(new Fib { A = 0, B = 1 })
.Concat(from f in self where f.B < 100 select new Fib { A = f.B, B = f.A + f.B }));
List<Fib> results = await (from f in fib select f).ToListAsync();
// results[0] = { A = 0, B = 1 }
// results[11] = { A = 89, B = 144 }Generated SQL:
WITH RECURSIVE cte0 AS (
SELECT f1."A" AS "A",
f1."B" AS "B"
FROM (SELECT @p2 AS "A", @p3 AS "B") AS f1
UNION ALL
SELECT f2."B" AS "A",
(f2."A" + f2."B") AS "B"
FROM cte0 AS f2
WHERE f2."B" < @p4
)
SELECT f0."A" AS "A",
f0."B" AS "B"
FROM cte0 AS f0Walk a reporting hierarchy and assign a depth level to each person. This uses two CTEs, the first wraps the table, the second is the recursive traversal:
class Org { public required string Name { get; set; } public string? Boss { get; set; } }
class OrgLevel { public required string Name { get; set; } public int Level { get; set; } }
SQLiteCte<Org> org = db.With(() => db.Table<Org>());
SQLiteCte<OrgLevel> hierarchy = db.WithRecursive<OrgLevel>(self =>
(from o in org where o.Boss == null select new OrgLevel { Name = o.Name, Level = 1 })
.Concat(from o in org
join p in self on o.Boss equals p.Name
select new OrgLevel { Name = o.Name, Level = p.Level + 1 }));
List<OrgLevel> result = await (from n in hierarchy orderby n.Level, n.Name select n).ToListAsync();Generated SQL:
WITH RECURSIVE cte0 AS (
SELECT o2."Name" AS "Name",
o2."Boss" AS "Boss"
FROM "Org" AS o2
),
cte1 AS (
SELECT o1."Name" AS "Name",
@p1 AS "Level"
FROM cte0 AS o1
WHERE o1."Boss" IS NULL
UNION ALL
SELECT o3."Name" AS "Name",
(o4."Level" + @p2) AS "Level"
FROM cte0 AS o3
JOIN cte1 AS o4 ON o3."Boss" = o4."Name"
)
SELECT o0."Name" AS "Name",
o0."Level" AS "Level"
FROM cte1 AS o0
ORDER BY o0."Level" ASC, o0."Name" ASCFind everyone who reports to Alice, directly or indirectly, without duplicates:
class WorksFor { public required string Name { get; set; } }
SQLiteCte<Org> org = db.With(() => db.Table<Org>());
SQLiteCte<WorksFor> worksFor = db.WithRecursive<WorksFor>(self =>
(from o in org where o.Name == "Alice" select new WorksFor { Name = o.Name })
.Union(from o in org
join w in self on o.Boss equals w.Name
select new WorksFor { Name = o.Name }));
List<WorksFor> result = await (from w in worksFor select w).ToListAsync();Generated SQL:
WITH RECURSIVE cte0 AS (
SELECT o2."Name" AS "Name",
o2."Boss" AS "Boss"
FROM "Org" AS o2
),
cte1 AS (
SELECT o1."Name" AS "Name"
FROM cte0 AS o1
WHERE o1."Name" = @p0
UNION
SELECT o3."Name" AS "Name"
FROM cte0 AS o3
JOIN cte1 AS w1 ON o3."Boss" = w1."Name"
)
SELECT w0."Name" AS "Name"
FROM cte1 AS w0This example is taken directly from the SQLite WITH documentation. It solves a Sudoku puzzle entirely in SQL using three CTEs. One is for the input grid, one enumerates digits 1 to 9 and one is a recursive CTE that fills in blanks one at a time using NOT EXISTS to check row, column and box constraints.
The puzzle string is 81 characters where . marks an empty cell.
class SudInput { public string Sud { get; set; } = ""; }
class SudDigit { public string Z { get; set; } = ""; public int Lp { get; set; } }
class SudX { public string S { get; set; } = ""; public int Ind { get; set; } }
const string puzzle = "53..7....6..195....98....6.8...6...34..8.3..17...2...6.6....28....419..5....8..79";
SQLiteCte<SudInput> input = db.With(() => db.Values(new SudInput { Sud = puzzle }));
SQLiteCte<SudDigit> digits = db.WithRecursive<SudDigit>(self =>
db.Values(new SudDigit { Z = "1", Lp = 1 })
.Concat(from d in self where d.Lp < 9
select new SudDigit { Z = (d.Lp + 1).ToString(), Lp = d.Lp + 1 }));
SQLiteCte<SudX> x = db.WithRecursive<SudX>(self =>
(from i in input select new SudX { S = i.Sud, Ind = i.Sud.IndexOf('.') + 1 })
.Concat(
from xr in self
from z in digits
where xr.Ind > 0
&& !(from lp in digits
where z.Z == xr.S.Substring(((xr.Ind - 1) / 9) * 9 + lp.Lp - 1, 1)
|| z.Z == xr.S.Substring((xr.Ind - 1) % 9 + (lp.Lp - 1) * 9, 1)
|| z.Z == xr.S.Substring(((xr.Ind - 1) / 3) % 3 * 3 + ((xr.Ind - 1) / 27) * 27 + lp.Lp + ((lp.Lp - 1) / 3) * 6 - 1, 1)
select lp).Any()
select new SudX {
S = xr.S.Substring(0, xr.Ind - 1) + z.Z + xr.S.Substring(xr.Ind),
Ind = (xr.S.Substring(0, xr.Ind - 1) + z.Z + xr.S.Substring(xr.Ind)).IndexOf('.') + 1
}
));
string solution = await (from xr in x where xr.Ind == 0 select xr.S).FirstAsync();
// solution = "534678912672195348198342567859761423426853791713924856961537284287419635345286179"The key LINQ features used:
-
from xr in self from z in digitsbecomesCROSS JOINfor trying each digit at each blank -
!(from lp in digits where ... select lp).Any()becomesNOT EXISTS (SELECT 1 FROM ... WHERE ...) -
xr.S.Substring(offset, 1)becomesSUBSTR(s, offset + 1, 1) -
xr.S.IndexOf('.')becomesINSTR(s, '.') - 1 -
(d.Lp + 1).ToString()becomesCAST((lp + 1) AS TEXT)
Traverse a parent-child hierarchy starting from root nodes:
SQLiteCte<Category> tree = db.WithRecursive<Category>(self =>
db.Table<Category>().Where(c => c.ParentId == null)
.Concat(
from c in db.Table<Category>()
join p in self on c.ParentId equals p.Id
select c));
List<Category> allNodes = await (from c in tree select c).ToListAsync();