Releases: destbg/SQLite.Framework
Release list
v6.1.0
Release notes for 6.1.0
Adds three new aggregate features: group_concat, total, and filtered aggregates.
String join with group_concat
string.Join over an IQueryable now translates to SQLite's group_concat. The rows are joined into one string inside SQL instead of being pulled into memory.
var titlesPerAuthor = await (
from a in db.Table<Author>()
select new
{
Author = a.Name,
Titles = string.Join(", ", db.Table<Book>()
.Where(b => b.AuthorId == a.Id)
.Select(b => b.Title))
}
).ToListAsync();For a single roundtrip at the root of a query, use the new StringJoin and StringJoinAsync extensions:
string allTitles = db.Table<Book>()
.OrderBy(b => b.Id)
.Select(b => b.Title)
.StringJoin(", ");A plain string.Join(sep, queryable) at the root still works but enumerates rows in memory. Use StringJoin when you want SQL to do the work.
Total aggregate
SQLiteFunctions.Total(...) translates to SQLite's total(X). It is like Sum but always returns a REAL value and returns 0.0 for empty input instead of NULL.
var revenueByAuthor = await (
from book in db.Table<Book>()
group book by book.AuthorId into g
select new
{
AuthorId = g.Key,
Revenue = SQLiteFunctions.Total(g.Select(b => b.Price))
}
).ToListAsync();At the root of a query, call Total or TotalAsync directly on the queryable:
double revenue = db.Table<Book>()
.Where(b => b.AuthorId == 1)
.Total(b => b.Price);Filtered aggregates
Chain a Where before any grouping aggregate to restrict which rows it sees. This emits SQLite's FILTER (WHERE ...) clause (SQLite 3.30+).
var rows = await (
from book in db.Table<Book>()
group book by book.AuthorId into g
select new
{
AuthorId = g.Key,
PriceyRevenue = g.Where(x => x.Price >= 10).Sum(x => x.Price),
TotalRevenue = g.Sum(x => x.Price)
}
).ToListAsync();Full Changelog: 6.0.1...6.1.0
v6.0.1
Release notes for 6.0.1
This version only updates nuget's README to properly point to the new https://sqlite-framework.net/ site.
Full Changelog: 6.0.0...6.0.1
v6.0.0
Changes from 5.3.2 to 6.0.0
How to upgrade
The separateConnection: true flag is gone from every transaction method and from the range write methods.
Before (5.3.2):
using SQLiteTransaction tx = db.BeginTransaction(separateConnection: true);
db.Table<Book>().AddRange(books, separateConnection: true);After (6.0.0):
using SQLiteTransaction tx = db.BeginTransaction();
db.Table<Book>().AddRange(books);The feature did not deliver what its name implied. SQLite serializes writers at the file level no matter how many connections you open, so opening a side connection never gave you parallel writes. After measuring the two paths against each other, the separate path was slower because of the extra connection setup.
Breaking changes
separateConnection removed
Removed from BeginTransaction, BeginTransactionAsync, and the range methods (AddRange, UpdateRange, RemoveRange, AddOrUpdateRange, UpsertRange) on SQLiteTable<T> and SQLiteReturningTable<T, TResult>, sync and async. Drop the argument where you pass it.
Async methods moved to extension methods
Some async methods that used to live on the core classes are now extension methods.
SQLiteDatabase.LockAsyncandReadLockAsyncare now onAsyncDatabaseExtensions.SQLiteTransaction.CommitAsyncandRollbackAsyncare now onAsyncTransactionExtensions.
Obsolete table shims removed
SQLiteTable<T>.CreateTable(), SQLiteTable<T>.DropTable(), and their async equivalents (CreateTableAsync, DropTableAsync) were marked obsolete in 5.0.0 and are now removed. Use db.Schema.CreateTable<T>(), db.Schema.DropTable<T>(), and the async variants on AsyncSchemaExtensions instead.
New features
Minimum SQLite version
The same code can land on Android 5 (SQLite 3.8.6) and Android 14 (3.41), or iOS 11 (3.19) and iOS 17 (3.43). A method that compiles fine fails at runtime on the older device.
Declare the floor your app commits to:
SQLiteOptions options = new SQLiteOptionsBuilder("app.db")
.UseMinimumSqliteVersion(SQLiteMinimumVersion.V3_35)
.Build();At connection open and on method calls the framework checks the loaded SQLite meets the floor. Any later call that needs a newer SQLite version throws a clear NotSupportedException naming the feature and the required version, instead of falling through to SQLite's no such function.
The enum has an entry for every SQLite minor release from 3.8 through 3.50, with each entry naming the iOS and Android versions whose system SQLite meets that floor. Available on the main SQLite.Framework package and on SQLite.Framework.Base. The SQLite.Framework.Bundled and SQLite.Framework.Cipher packages ship a SQLite with a known version, so the floor is forced to Unspecified.
R-Tree virtual tables
A class can now be mapped to a SQLite R-Tree virtual table with [RTreeIndex]. The framework reads [Key], [RTreeMin], [RTreeMax], and [RTreeAuxiliary] to build the schema, supports Add, Update, Remove, and queries against the bounding box. Both rtree (float) and rtree_i32 (integer) modules are supported. Requires SQLite 3.24.0 or newer.
[RTreeIndex]
public class Region
{
[Key] public int Id { get; set; }
[RTreeMin("X")] public float MinX { get; set; }
[RTreeMax("X")] public float MaxX { get; set; }
[RTreeMin("Y")] public float MinY { get; set; }
[RTreeMax("Y")] public float MaxY { get; set; }
[RTreeAuxiliary] public string? Label { get; set; }
}
db.Schema.CreateTable<Region>();
db.Table<Region>().Add(new Region { Id = 1, MinX = 0, MaxX = 10, MinY = 0, MaxY = 10, Label = "A" });
List<Region> hits = db.Table<Region>()
.Where(r => r.MinX <= 5 && r.MaxX >= 5 && r.MinY <= 5 && r.MaxY >= 5)
.ToList();STRICT tables
Mark a class with [StrictTable] to create the table in SQLite STRICT mode. SQLite then enforces the declared column types on every insert and update instead of accepting any type. Requires SQLite 3.37.0 or newer.
[StrictTable]
public class Book
{
[Key]
public int Id { get; set; }
public required string Title { get; set; }
}The fluent builder has the same option:
db.Table<Book>().Schema.Strict().CreateTable();[StrictTable] combines with [WithoutRowId]. The framework writes the two modifiers as WITHOUT ROWID, STRICT.
Stream-based BLOB I/O
SQLiteDatabase.OpenBlobStream(...) returns a SQLiteBlobStream (a Stream) over a single BLOB column on a single row, using SQLite's incremental BLOB I/O.
using SQLiteBlobStream stream = db.OpenBlobStream<Book>(book.Id, b => b.Cover);
await stream.CopyToAsync(outputFile);
db.Table<Book>().Add(new Book { Id = 1, Cover = new byte[payload.Length] });
using SQLiteBlobStream write = db.OpenBlobStream<Book>(1, b => b.Cover, writable: true);
await write.WriteAsync(payload);The stream is fixed-length, SQLite incremental I/O cannot grow a blob. Pre-allocate with a sized byte[] through Add or Update, or with raw SQL using zeroblob(n). Overloads accept either a strongly-typed column selector or raw table and column names. OpenBlobStreamAsync waits for the connection lock asynchronously.
UPDATE FROM
ExecuteUpdate now accepts a Join chain. SQLite emits the joined sources as a FROM clause and the join keys as WHERE conditions. Requires SQLite 3.33.0 or newer.
db.Table<Book>()
.Join(db.Table<Author>(), b => b.AuthorId, a => a.Id, (b, a) => new { b, a })
.Where(x => x.a.Country == "US")
.ExecuteUpdate(s => s.Set(x => x.b.Title, x => x.a.Name + " - bestseller"));The Set lvalue must reference the target table (the first Table<T> in the chain). Referencing a joined source throws.
COLLATE in indexes
Indexes can now declare a collation per column. Useful for case-insensitive or trailing-space-insensitive lookups that the query planner can actually use.
[Indexed(Collation = SQLiteCollation.NoCase)]
public required string Email { get; set; }The fluent builder takes a single collation for all columns, or an array for per-column control on a composite index:
db.Table<Book>().Schema
.Index(b => b.Email, unique: true, collation: SQLiteCollation.NoCase)
.Index(b => new { b.LastName, b.FirstName },
collations: [SQLiteCollation.NoCase, SQLiteCollation.NoCase])
.CreateTable();SQLiteCollation has four values: Inherit (no clause emitted, the column's declared collation wins), Binary, NoCase, and Rtrim. For ad-hoc query use, SQLiteFunctions.Collate(expr, collation) wraps an expression in a COLLATE clause.
Expression and functional indexes
The fluent Index(...) builder now accepts any expression body, so b => b.Title.ToLower() and composite slots that mix columns with expressions create an expression index. Requires SQLite 3.9.0 or newer.
db.Table<Book>().Schema
.Index(b => b.Title.ToLower(), name: "IX_Book_TitleLower")
.CreateTable();Per-column ASC and DESC in indexes
Index(...) and [Indexed] now accept a direction for every slot of the index. The default SQLiteIndexDirection.Inherit emits no clause, Ascending emits ASC, and Descending emits DESC.
db.Table<Book>().Schema
.Index(b => new { b.AuthorId, b.PublishedAt },
name: "IX_Book_AuthorAndPublished",
directions: [SQLiteIndexDirection.Ascending, SQLiteIndexDirection.Descending])
.CreateTable();Column defaults
A column can declare its DEFAULT clause via the [DefaultValue] attribute, the fluent builder, or the AddColumn migration helper.
[DefaultValue(0)]
public int Rating { get; set; }
[DefaultValue("Unknown")]
public string Genre { get; set; }The fluent builder has two overloads. One for literals and one for a translated SQL expression:
db.Table<Book>().Schema
.Default(b => b.Rating, 0)
.Default(b => b.Slug, () => SQLiteFunctions.SqliteVersion())
.CreateTable();AddColumn accepts the same two shapes:
db.Schema.AddColumn<Book>(b => b.Rating, () => 7 * 6);MATERIALIZED and NOT MATERIALIZED CTE hints
db.With(...) and db.WithRecursive(...) now accept a SQLiteCteMaterialization argument. The default emits no hint. Materialized emits AS MATERIALIZED (, NotMaterialized emits AS NOT MATERIALIZED (. Requires SQLite 3.35.0 or newer to set a non-default value.
SQLiteCte<Book> cte = db.With(
() => db.Table<Book>().Where(b => b.Price > 100),
SQLiteCteMaterialization.Materialized);VACUUM and VACUUM INTO
SQLiteDatabase now exposes Vacuum(schema?) to rebuild the database file and VacuumInto(path, schema?) to write a clean copy to a separate file. Both have async wrappers. VACUUM is universally supported. VACUUM INTO requires SQLite 3.27.0 or newer.
db.Vacuum();
db.VacuumInto("backup.db");REINDEX
SQLiteDatabase.Reindex(name?) rebuilds indexes. No argument rebuilds every index in every attached database. Pass a table, index, or collation name to limit the scope. ReindexAsync is also available.
db.Reindex();
db.Reindex("Books");
db.Reindex("NOCASE");Direct date and time SQL functions
A new SQLiteDateFunctions class exposes SQLite's date and time functions with modifier strings: Date, Time, Datetime, JulianDay, Strftime, Timediff. Each accepts a time value plus any number of modifier strings like "+7 days", "start of month", "unixepoch", or "utc". Timediff (SQLite 3.43.0+) is not available in SQLCipher and emits a compile-time error there.
string month = db.Table<Book>()
.Select(...v5.3.2
Release notes for 5.3.2
AddColumn now accepts a default value.
AddColumn default value
AddColumn and AddColumnAsync take an optional defaultValue argument. When set, the framework writes it as the SQL DEFAULT clause and SQLite uses it to fill existing rows.
The emitted SQL is:
ALTER TABLE "Books" ADD COLUMN Pages INTEGER NOT NULL DEFAULT 0
ALTER TABLE "Books" ADD COLUMN Genre TEXT NULL DEFAULT 'Unknown'Full Changelog: 5.3.1...5.3.2
v5.3.1
Release notes for 5.3.1
Composite indexes are now correctly working for both the attribute layer and the fluent builder.
Composite indexes
When multiple properties carry [Indexed] with the same name, the framework now groups them into one CREATE INDEX statement ordered by Order. Previously, each attribute produced its own single-column index, which was ignored at runtime when two properties shared the same index name.
[Indexed("IX_Book_AuthorGenre", 0)]
public int AuthorId { get; set; }
[Indexed("IX_Book_AuthorGenre", 1)]
public string? Genre { get; set; }This now produces a single index:
CREATE INDEX "IX_Book_AuthorGenre" ON "Books" (AuthorId, Genre)Set IsUnique = true on any of the attributes to make the composite index unique.
The fluent builder also accepts an anonymous object to create a composite index in one call:
db.Schema.Table<Book>()
.Index(b => new { b.AuthorId, b.Genre }, name: "IX_Book_AuthorGenre")
.CreateTable();Full Changelog: 5.3.0...5.3.1
v5.3.0
Release notes for 5.3.0
Foreign keys, SQLite's RETURNING clause, one-call JSON converter registration and faster row materialization.
Foreign keys
Declare a foreign key with [ReferencesTable(typeof(Parent))]. For composite keys use the fluent db.Schema.Table<T>().ForeignKey<TParent>(...) builder.
public class Book
{
[Key]
public int Id { get; set; }
public required string Title { get; set; }
[ReferencesTable(typeof(Author), OnDelete = SQLiteForeignKeyAction.Cascade)]
public int AuthorId { get; set; }
}The framework also reads [System.ComponentModel.DataAnnotations.Schema.ForeignKey("Author")] (string target, primary key inferred, NoAction defaults). Use [ReferencesTable] when you need an action other than NoAction, deferred enforcement, or typeof targeting.
Enforcement is on by default. The framework runs PRAGMA foreign_keys = ON on every connection open. Pass UseForeignKeys(false) to the builder to opt out, or flip it at runtime with db.Pragmas.ForeignKeys = false.
RETURNING
Returning wraps a query or a table so the next write returns the affected rows instead of the row count. Requires SQLite 3.35 or later.
List<int> archivedIds = await db.Table<Book>()
.Where(b => b.Price > 100)
.Returning(b => b.Id)
.ExecuteDeleteAsync();
Book? added = await db.Table<Book>()
.Returning()
.AddAsync(new Book { Title = "Clean Code", AuthorId = 1, Price = 29.99m });The RETURNING clause can only reference columns from the table being modified. SQLite rejects projections that touch a joined entity.
Dictionary row materialization
Pass Dictionary<string, object?> (or Dictionary<string, object>) as the element type to get each row as a column-name to value map. Values come back in their SQLite-native form (long, double, string, byte[], or null).
List<Dictionary<string, object?>> rows = db.Query<Dictionary<string, object?>>(
"SELECT Id, Name FROM Books");
var streamed = db.CreateCommand("SELECT * FROM Books", [])
.ExecuteQuery<Dictionary<string, object?>>()
.ToList();JSON graph registration
SQLiteOptionsBuilder.AddJsonContext(context) and AddJsonbContext(context) register a JSON converter for every type in your JsonSerializerContext and walks nested property types. Saves the per-type AddTypeConverter<T>(new SQLiteJsonConverter<T>(...)) boilerplate.
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(Customer))]
public partial class AppJsonContext : JsonSerializerContext;
SQLiteOptions options = new SQLiteOptionsBuilder("app.db")
.AddJsonContext(AppJsonContext.Default)
.Build();AddJsonbContext does the same with JSONB storage. Both methods skip any type that already has a converter.
Faster row materialization
The runtime and source-generated materializers now resolve column indices once per query instead of once per row. The row body uses typed reader calls bound to those indices.
100-row entity read, .NET 10, JIT:
| Path | Before | After |
|---|---|---|
| SQLite.Framework + SourceGenerator | 39.0 μs / 21.8 KB | 36.5 μs / 22.1 KB |
| SQLite.Framework | 69.7 μs / 36.1 KB | 37.9 μs / 22.2 KB |
EntityMaterializers is now a builder dictionary
The EntityMaterializers map changed from Func<SQLiteQueryContext, object?> (per row) to Func<SQLiteQueryContext, Func<SQLiteQueryContext, object?>> (factory per query that returns the row materializer).
If you registered a materializer manually, change it like this:
// Before
builder.EntityMaterializers[typeof(Book)] = ctx =>
{
int id = (int)ctx.Reader!.GetValue(ctx.Columns!["Id"], ctx.Reader.GetColumnType(ctx.Columns["Id"]), typeof(long))!;
return new Book { Id = id, /* ... */ };
};
// After
builder.EntityMaterializers[typeof(Book)] = setupCtx =>
{
int idIndex = setupCtx.Columns!["Id"];
return rowCtx => new Book { Id = rowCtx.Reader!.GetInt32(idIndex), /* ... */ };
};The source-generated UseGeneratedMaterializers extension is updated automatically when you bump SQLite.Framework.SourceGenerator.
Platform support
A few public APIs that needed a newer SQLite than older iOS / Android system SQLite ships with now carry [SupportedOSPlatform] attributes, so CA1416 warns when you target an OS version below the floor. Only applies to the SQLite.Framework package.
| API | Min iOS | Min Android | SQLite |
|---|---|---|---|
SQLiteJsonFunctions JSON1 methods (Extract, Set, Insert, Replace, Remove, Type, Valid, Patch, ArrayLength, Minify) |
10.0 | 24.0 | 3.9 |
SQLiteFunctions.UnixEpoch() / UnixEpoch(string) |
16.0 | 34.0 | 3.38 |
SQLitePragmas.TableInfo / IndexList / ForeignKeyList |
11.0 | 26.0 | 3.16 |
SQLiteOptionsBuilder.AddJsonbContext (new this release) |
unsupported | 36.0 | 3.45 |
Returning and the SQLiteReturningQueryable / SQLiteReturningTable write methods (new this release) |
15.0 | 34.0 | 3.35 |
Full Changelog: 5.2.0...5.3.0
v5.2.0
Release notes for 5.2.0
This release adds typed access to SQLite's internal tables and pragma table-valued functions, makes LINQ subquery syntax work, and ships a few new query and configuration helpers.
Pragmas and system tables
db.Pragmas now exposes typed wrappers for the SQLite tables and table-valued functions used to inspect a database.
db.Pragmas.Masteris a LINQ-queryable view oversqlite_master.db.Pragmas.Sequenceis a LINQ-queryable view oversqlite_sequence.db.Pragmas.TableInfo(name)wrapspragma_table_info.db.Pragmas.IndexList(name)wrapspragma_index_list.db.Pragmas.ForeignKeyList(name)wrapspragma_foreign_key_list.
The pragma wrappers can be used inside an outer query, with the argument bound to a column of the outer source. The framework emits a single SQL statement.
var rows = (
from m in db.Pragmas.Master
from p in db.Pragmas.TableInfo(m.Name)
where m.Type == "table" && !m.Name.StartsWith("sqlite_")
select new { Table = m.Name, Column = p.Name, Type = p.Type }
).ToList();LINQ subquery syntax emits real SQL subqueries
When the source of a from is a query that contains Select, Where, GroupBy, Take, Skip, Distinct, or Reverse, the framework now wraps it as a real SQL subquery instead of flattening it into the root query.
from b in (from a in db.Table<Book>() where a.AuthorId == 1 select a)
from u in db.Table<Author>()
select new { b.Title, u.Name }emits
SELECT b1.Title, a1.AuthorName
FROM ( SELECT ... FROM "Books" WHERE BookAuthorId = @p0 ) AS b1
CROSS JOIN "Authors" AS a1Read-only tables
db.ReadOnlyTable<T>() returns a ReadOnlySQLiteTable<T> with the full LINQ surface (Select, Where, Join, OrderBy, and so on) but without Add, Update, Remove, or Clear. Used by the new pragma tables.
WhereBuilder
IQueryable<T>.WhereBuilder(...) lets you compose a single WHERE clause from many AND / OR predicates. Helpful when filters are built up at runtime and chaining Where would not capture the desired logic.
var query = db.Table<Book>().WhereBuilder(b =>
{
b.And(x => x.Price > 0);
if (searchTerm != null)
{
b.Or(x => x.Title.Contains(searchTerm));
b.Or(x => x.Author.Contains(searchTerm));
}
});An empty builder leaves the query unchanged.
Command interceptors and logging
ISQLiteCommandInterceptor (in SQLite.Framework) lets you observe every command the framework runs. Methods are OnExecuting(SQLiteCommand), OnExecuted(SQLiteCommand, int? rowsAffected), and OnFailed(SQLiteCommand, Exception). Register through the options builder:
builder.AddCommandInterceptor(new MyInterceptor());For a quick log of every SQL statement, use the shorthand:
builder.LogCommands(Console.WriteLine);Output looks like:
(12ms, 3 rows) SELECT b0.BookId AS "Id", b0.BookTitle AS "Title" FROM "Books" AS b0 WHERE b0.BookAuthorId = @p0 | @p0=?
Parameter values are masked with ? by default. Call EnableSensitiveParameterLogging() on the builder to inline real values in the log output.
The previous SQLiteDatabase.CommandCreated event has been replaced by the new interceptor surface, which also reports the elapsed time and outcome.
Views and triggers
db.Schema now has typed builders for views and triggers.
db.Schema.CreateView<BookSummary>(() =>
from b in db.Table<Book>()
where b.Price > 0
select new BookSummary { Id = b.Id, Title = b.Title, Price = b.Price });
bool exists = db.Schema.ViewExists<BookSummary>();
db.Schema.DropView<BookSummary>();
db.Schema.CreateTrigger<Book>(
name: "trg_book_history",
timing: TriggerTiming.After,
@event: TriggerEvent.Update,
body: "INSERT INTO BookHistory(BookId, OldPrice, NewPrice) VALUES (NEW.Id, OLD.BookPrice, NEW.BookPrice)",
when: "OLD.BookPrice <> NEW.BookPrice");
db.Schema.DropTrigger("trg_book_history");The view name comes from the [Table("...")] attribute on the entity. Pair the view with db.ReadOnlyTable<T>() to query it. Trigger bodies and the optional WHEN predicate are raw SQL strings that use OLD and NEW to refer to the row. Async wrappers (CreateViewAsync, DropViewAsync, ViewExistsAsync, ListViewsAsync, CreateTriggerAsync, DropTriggerAsync) are also provided.
EXPLAIN QUERY PLAN
IQueryable<T>.ExplainQueryPlan() runs EXPLAIN QUERY PLAN for the query and returns a tree of SQLiteQueryPlanNodes. Useful for checking whether the planner picks the index you expect.
SQLiteQueryPlan plan = db.Table<Book>()
.Where(b => b.AuthorId == 1)
.ExplainQueryPlan();
Console.WriteLine(plan);prints
QUERY PLAN
> SEARCH b0 USING INDEX IX_Book_AuthorId (BookAuthorId=?)
ToString() renders an ASCII tree with two-space indents. There is also ExplainQueryPlanAsync for the async path.
Configuration
SQLiteOptionsBuilder.UseBlockReadsDuringTransaction()makes reads from other async contexts wait for the active transaction to finish. Off by default.
Platform support
SQLiteSchema.RenameColumn, SQLiteSchema.DropColumn, the matching async wrappers, and the fluent Computed (generated columns) builder method are now marked with SupportedOSPlatform attributes. iOS and Android projects targeting an OS version older than the one that ships the required SQLite get a build-time warning.
| Method | Min iOS | Min Android | SQLite version |
|---|---|---|---|
ExplainQueryPlan / ExplainQueryPlanAsync |
12.0 |
30.0 |
3.24.0 |
RenameColumn / RenameColumnAsync |
13.0 |
30.0 |
3.25.0 |
Computed (fluent generated columns) |
14.0 |
31.0 |
3.31.0 |
DropColumn / DropColumnAsync |
15.0 |
34.0 |
3.35.0 |
Only applies to the SQLite.Framework package (which uses the OS-provided SQLite on iOS and Android). The SQLite.Framework.Bundled and SQLite.Framework.Base packages do not get the attributes.
JSON
- Now handles more shapes, including nested collections inside projections and mixes of JSON paths with regular columns.
- Now correctly handles
SelectManyover JSON arrays and propagates the element type through chained operators.
Async
- Async methods now don't block the background thread while waiting for a transaction to finish.
- The async extension methods now cover
LongCount,ToDictionarywithout an element selector, and pass cancellation tokens by parameter onAdd,Update,Remove,AddOrUpdate, andUpsertoverloads. - Async queryable methods now rethrow the inner exception cleanly so call-site stack traces are no longer wrapped in an
AggregateException. - New
AsyncSQLiteCommandExtensionslets you awaitExecuteNonQuery,ExecuteScalar,ExecuteQuery, andExecuteReaderdirectly onSQLiteCommand.
Window functions
The window builder accepts more frame shapes (rows-between, range-between, groups-between) and the Window accessor on IGrouping now flows through OrderBy / ThenBy.
Internals
SQLiteExpressionwas reworked to reduce allocations and increase speed.
Samples
- New ASP.NET sample under
Sample/SQLite.Framework.AspNet.
Package updates
SQLite.Framework.Basenow ships withSQLitePCLRaw.coreversion3.0.3.SQLite.Framework.Bundlednow ships withSQLitePCLRaw.bundle_e_sqlite3version3.0.3.
Full Changelog: 5.1.0...5.2.0
v5.1.0
Release notes for 5.1.0
This release is focused on expanding LINQ translations and improving exceptions in queries.
New LINQ translations
Math
Math.Sin,Math.Cos,Math.Tanmap toSIN,COS,TAN.Math.Asin,Math.Acos,Math.Atan,Math.Atan2map toASIN,ACOS,ATAN,ATAN2.Math.Sinh,Math.Cosh,Math.Tanh,Math.Asinh,Math.Acosh,Math.Atanhmap to the matching SQLite hyperbolic functions.Math.Cbrt(x)maps toPOWER(..., 1.0/3.0)and keeps the sign, so it works for negative numbers too.Math.Log2(x)maps toLOG2(x).Math.Clamp(x, min, max)maps to aCASE WHEN ... THEN ... ELSE ... END.Math.Truncate(x)maps toTRUNC(x).Math.Round(x, digits, MidpointRounding.AwayFromZero)is now translated, so the 3-argument overload no longer needs to run on the client.
String
s[i](string indexer) maps toSUBSTR(s, i + 1, 1).s.CompareTo(other)maps toCASE WHEN s = other THEN 0 WHEN s < other THEN -1 ELSE 1 END, the same shape asstring.Compare.
DateTime / DateTimeOffset
DateandTimeOfDayare now translated for bothDateTimeandDateTimeOffset. Before, they were silently passed through with no SQL.
Nullable
Nullable<T>.GetValueOrDefault()maps toCOALESCE(x, 0).Nullable<T>.GetValueOrDefault(default)maps toCOALESCE(x, default).
Collections
IEnumerable.Containsover a constant collection now also works when the compiler wraps the array in an implicit conversion (for example, C# 12 turnsint[]intoReadOnlySpan<int>viaop_Implicit). The visitor unwraps the conversion and emitsIN (...).GroupBy(x => new { x.A, x.B })now lets you read the composite key asg.Key.Aandg.Key.Bin the projection. Before, those member accesses could not be resolved.Distinctnow correctly applies insideSum,Avg, andCountaggregates asSUM(DISTINCT col)and so on, when chained as.Select(...).Distinct().Sum().
Behavior fixes
- Constructor not being accepted for entity materialization: used to throw an exception. Now works as intended, this also makes it possible to use primary constructor record types.
Sumon an empty source: used to throwNullReferenceException. Now returns0for non-nullable numeric types viaCOALESCE(SUM(...), 0). Nullable result types still returnnull.Max,Min,Averageon an empty source: used to throwNullReferenceException. Now throwsInvalidOperationException("Sequence contains no elements")for non-nullable result types, the same way LINQ-to-Objects does. Nullable result types returnnull.Take(N).Take(M): the second call used to replace the first, soTake(2).Take(5)returned 5 rows. Now it takes the smaller of the two values.Skip(N).Skip(M): the second call used to replace the first. Now both values are added together, so the total skip isN + M.Take(N).Skip(M): used to translate directly toLIMIT N OFFSET M, which is the wrong order for LINQ. Now it follows LINQ rules: takeNrows first, then skipMof those. The effective limit becomesmax(0, N - M)and the offset addsM.OrderBy(...).OrderByDescending(...): the second call did not reset the sort because the check had a typo (nameof(Queryable.OrderDescending)instead ofOrderByDescending). It silently behaved likeThenByDescending. Fixed.- Aggregates after
TakeorSkip: used to produce wrong SQL becauseLIMIT/OFFSETwas placed next toCOUNT(*)orSUM(...). Now throws a clearNotSupportedExceptionand tells the user to load the rows into memory first. - Operations after
Concat,Union,Intersect,Except:Where, non-identitySelect,Distinct,GroupBy, aggregates,Reverse,Contains,Join,SelectMany, and the predicate forms ofFirst,Single,Any,Allused to produce silently wrong SQL, because the operation was applied only to the first query and the second was left as-is. All of these now throw a clearNotSupportedExceptionand suggest applying the operation to each side before combining, or loading the rows into memory first.OrderBy,Take,Skip, the identitySelect, the no-predicate forms ofFirstandAny, and chained set operations still work as before. - Aggregate after
Distincton a multi-column projection: now throws a clearNotSupportedExceptioninstead of building invalid SQL. GroupJoin(the LINQinto <name>form) used as a sequence: when the result selector calls sequence methods on the group (such asbg.Count()orbg.Sum(...)) instead of flattening withfrom x in bg.DefaultIfEmpty(), the framework now throws a clear error and points to the correlated-subquery shape.- Chained scalar
Selectwith a method or property access: code likedb.Table<X>().Select(x => x.Title).Select(t => t.Length)used to drop the second member access and emitSELECT Title AS "Length", which then crashed when the runtime tried to read the string as an int. The visitor now recognises that the outer parameter is a single-column projection and applies the member access (soLengthbecomesLENGTH(Title)). - Member access on the result of
.First/.Single/etc. when the source returns an entity: code likedb.Table<X>().First(x => p).Nameused to emit(SELECT * FROM X WHERE p)and SQLite then complainedsub-select returns 4 columns - expected 1. The visitor now throws a clearNotSupportedExceptionpointing the user to the.Where(p).Select(x => x.Name).First()rewrite. string.Equals(other, StringComparison): the instance overload silently dropped theStringComparisonargument.b.Title.Equals("apple", StringComparison.OrdinalIgnoreCase)translated tob.Title = 'apple', which is case-sensitive in SQLite and so never matched. The visitor now appliesCOLLATE NOCASEfor the three case-insensitive comparisons (OrdinalIgnoreCase,CurrentCultureIgnoreCase,InvariantCultureIgnoreCase) and leaves the other modes case-sensitive, matching the existingstring.Equals(a, b, StringComparison)static overload.
Source generator
- Anonymous-type materializers now handle nested entity initialization correctly.
- Registration for source-generated column writers was improved together with the materializer changes.
- Runtime materializer (the reflection path used when the source generator is not in use) now also accepts entities with a private parameterless constructor, matching what the source generator's reflection emitter has always supported.
Samples
- New Avalonia sample under
Sample/SQLite.Framework.Avalonia. - Both MAUI and Avalonia samples now ship a
SQLITE_FRAMEWORK.mdcheat sheet you can drop into a project so coding agents have an accurate reference.
Documentation
- New
wiki/Overview.mdpage, used as the landing page for the documentation site. - New
wiki/AI Assistance.mdpage that explains how to point Claude Code, Cursor, Copilot, and other agents at the cheat sheet. wiki/Expressions.mdupdated with all the new Math, String, and DateTime translations.
Full Changelog: 5.0.3...5.1.0
v5.0.3
- Fixed:
AddOrUpdatenow respects a non-default[AutoIncrement]primary key set on the entity. - New option:
PreserveExplicitAutoIncrementKeys()makesAddandAddRangeuse a non-default[AutoIncrement]key the same way EF Core does. - New wiki page: a guide for people moving from Entity Framework Core.
- Wiki upgrade: full-text search across all pages, offline support through a service worker and PWA manifest, and several smoothness fixes.
Full Changelog: 5.0.2...5.0.3
v5.0.2
AddRange, UpdateRange and other Range methods have had a more than 3x performance increase with around 10x reduced memory allocation
Full Changelog: 5.0.1...5.0.2