Repository navigation
Release/2.5.0 - #130
Merged
Merged
Release/2.5.0#130
Conversation
MyMovie bridged a database query to androidx.paging by hand, and got it wrong twice before a test with a real Pager found it: an offset-keyed source invalidated by a mediator needs a prevKey, or a refresh leaves everything above the user's position unreachable; with placeholders off, the anchorPosition is relative to the presented list, not an absolute offset; and offsets have to stay on the page size grid, or neighbouring pages overlap. A source provided by SQLlin, built on observable queries, as Room provides LimitOffsetPagingSource, would spare every consumer that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Table.UNION {} returned a statement typed by the table's row type, but read
the rows with its first SELECT's deserializer, through unchecked casts. A
union of projections, result columns or joins therefore compiled, and failed
with a ClassCastException wherever its results were used; with members of
different types and the same number of columns, values could also land in
the wrong properties without any error, as SQL matches the columns by
position and SQLlin reads them by name. The SQL was the members' SQL joined
with UNION, so a member's ORDER BY or LIMIT was a syntax error unless it was
the last one, where it applied to the whole union; the union itself could
take neither; and a nested block was flattened, so UNION { a; UNION_ALL { b;
c } } ran as (a UNION b) UNION ALL c. Each of these was checked with sqlite3.
The compound operators are now infix functions between two SELECTs, as in
SQL:
(table SELECT WHERE(age LT 18)) UNION (table SELECT WHERE(age GTE 65))
UNION, UNION_ALL, INTERSECT and EXCEPT take a SelectStatement<R> on both
sides, and SelectStatement is invariant, so mixing result types doesn't
compile; the compound reads its rows into that same R. The SELECTs can come
from any tables, through projections, result columns or joins. They become
part of the compound, removed from the statements of their scope, and their
deferred GROUP BY check runs when they are combined.
Chained operators are evaluated from left to right, as SQLite does. SQLite
has no parentheses in a compound, so a member that would mean something
else is written as a subquery, SELECT * FROM (...): one that ends with ORDER
BY, LIMIT or OFFSET, which then apply to it alone, and a compound on the
right, as in a UNION (b UNION_ALL c), which is then combined as a whole.
FinalSelectStatement.isSimple tells a plain SELECT, which needs none, from
one with OFFSET or a block's union. A CompoundSelectStatement can be followed
by another operator, and by ORDER BY and LIMIT, which apply to all of it.
As new DSL API, the operators, CompoundSelectStatement and the ORDER BY and
LIMIT that follow it require @OptIn(ExperimentalDSLDatabaseAPI::class).
The UNION {} and UNION_ALL {} blocks, and the beginUnion,
createUnionSelectStatement and endUnion functions they need for being
inline, are deprecated and not fixed. Their messages, KDoc and the guide say
why and point to the infix operators. The two blocks suppress the
deprecation of the functions they call, which are deprecated with them.
The guide's UNION section becomes one on compound queries, in English and
Chinese, ending with the deprecated block, its bugs and how to replace it.
The roadmap's UNION item moves to Supported.
Tests: testCompoundSelect covers the four operators, with WHERE parameters
whose order matters; rows of two tables projected into one type; that a
combined SELECT no longer runs alone; members with their own ORDER BY and
LIMIT, which fail without the subquery; the compound's ORDER BY, LIMIT and
OFFSET; left-to-right evaluation against grouping, which return 2 and 1 rows
on the same data; result columns; and rejecting a member that needs GROUP BY.
testUnionSelect keeps covering the deprecated block. Mixed result types and
missing parentheses fail to compile, and the opt-in warning appears without
@OptIn, checked with temporary files, as were the guide's examples. jvmTest
(54) and testAndroidHostTest on API 26 and 37 (108) pass. The native test
sources compile for macosArm64, linuxX64, mingwX64 and watchosArm32 but were
not run, as this machine is an Intel Mac.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setupNativeConfig got the main compilation with `by compilations.getting`
and created the sqlite3 cinterop with `by main.cinterops.creating`, which
Gradle 9.6 deprecates; the build script reported five warnings for them. They
are now getByName("main") and create("sqlite3"), as the deprecation messages
suggest. The cinterop's local val was never read, so it goes.
The cinterop keeps its name, so its tasks are still cinteropSqlite3<Target>.
Rerun from scratch, cinterop, compilation and test linking pass for
macosArm64 and linuxX64, the driver's jvmTest passes, the sqllin-dsl-test
sources compile for macosArm64, and the script compiles without warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DSLDBConfiguration, Database(DSLDBConfiguration), CREATE, CREATE_INDEX, CREATE_UNIQUE_INDEX, DROP, ALTER_ADD_COLUMN, both ALTER_RENAME_TABLE_TO, both RENAME_COLUMN, DROP_COLUMN and PRAGMA_FOREIGN_KEYS no longer carry @ExperimentalDSLDatabaseAPI, so using them no longer needs an opt-in. The marker stays on the compound SELECT operators added in 2.5.0, which are new. The opt-ins only those APIs needed go: the sample's, and the test class's and test functions', except testCompoundSelect's. The installation guide no longer calls DSLDBConfiguration experimental, and CHANGELOG.md says the opt-in can be removed. jvmTest (54) and testAndroidHostTest on API 26 and 37 (108) pass, the sample and the macosArm64 test sources compile, and nothing reports an opt-in warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MyMovie bolted change notification onto SQLlin by hand: its data source
announced every write to a tracker keyed by list, which a PagingSource
listened to. Every write had to remember to announce itself, and a write to
the movies table couldn't say which lists showed those movies, so they kept
their old titles.
Database.observe turns a SELECT into a Flow of its results:
val adults: Flow<List<Person>> = database.observe { PersonTable SELECT WHERE(PersonTable.age GTE 18) }
It emits the results when collected, and again whenever a statement run
through the database changes a table the SELECT reads, if the results
differ. The block runs again to build the SELECT each time. Queries run on
Dispatchers.IO unless another context is given.
Each SELECT now records the tables it reads: its table, the tables of a
join, and those of every member of a compound, carried along the chained
clauses.
The changes are counted by SQLite itself, as Room does. InvalidationTracker
creates a TEMP table of counts and, the first time a table is observed, TEMP
triggers on its INSERT, UPDATE and DELETE that add one to its count. So rows
changed by a foreign key action count though no statement names their
table, an UPDATE or DELETE matching no rows doesn't, and a rolled back
transaction rolls its counts back. The counts only grow, rather than being a
flag read and then cleared, as SQLlin can share a connection between
threads and a change between the read and the clear would be lost. A table
stays tracked until the connection closes, so triggers are never removed
while another observer may install them. On Android the counts are read in a
transaction, as a query outside one may run on a reader connection, which
has no TEMP table. After CREATE, DROP, ALTER or PRAGMA, missing triggers are
created again and every tracked table counts as changed.
Database runs each scope through execute, which reads the counts once the
statements ran, even when one failed, unless the scope only read. observe
tracks the SELECT's tables and notes their counts before running it, under
the same mutex as suspendedScope, so no later change is missed; equal
results aren't emitted again, and several changes in a row may lead to a
single query.
Only changes made through the same Database are seen, not those of another
instance, connection or process. Several SELECTs combine with Flow's
combine, which the guide notes may briefly mix new and old results. The
API is experimental, and kotlinx.coroutines becomes an api dependency of
sqllin-dsl, as the API returns a Flow.
Tests: testObserve covers the first results; an INSERT; no emission for a
change to another table, an UPDATE matching no rows, one leaving the results
equal, or a rolled back transaction; an UPDATE that changes them; a join
watching its second table; and a dropped and recreated table staying
watched. testObserveForeignKeyAction covers rows deleted by ON DELETE
CASCADE. jvmTest (56) and testAndroidHostTest on API 26 and 37 (112) pass, so
the triggers work on both. The native test sources compile for macosArm64,
linuxX64, mingwX64 and watchosArm32 but were not run, as this machine is an
Intel Mac. The guide's examples were compiled with a temporary file.
The guide gains a section on observed queries, in English and Chinese. The
roadmap's last high priority item moves to Supported.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A view is a stored SELECT that queries read like a table. SQLlin had no way to declare, create, query or drop one. A class annotated with @dbview gets a generated view object, AdultView for Adult, with a property per column like a table object, but no SET properties, no CREATE TABLE statement and no constraints. The processor rejects a constraint annotation on it, a class that is both @DBRow and @dbview, and a missing @serializable. CREATE_VIEW(AdultView) AS (PersonTable SELECT WHERE<Adult>(PersonTable.age GTE 18)) // CREATE VIEW adults(name,age) AS SELECT name,age FROM person WHERE age>=18 The SELECT has to read the view's row type, checked at compile time, and is taken from its scope as INSERT INTO ... SELECT does. SQLite doesn't let a view take parameters ("parameters are not allowed in views", checked with sqlite3), so inlineParameters writes the SELECT's values into its SQL as literals: strings with doubled quotes, BLOBs as X'..', Booleans as 1 and 0, NaN as NULL. A ? inside a string literal or a quoted identifier, as in replace(name, "?", ""), isn't a placeholder and stays. DROP(view) drops it with DROP VIEW. To keep views from being written to, Relation is the new base of Table and View. SELECT, joins, the SQL functions and the clause elements take a Relation; INSERT, UPDATE, DELETE, CREATE and ALTER keep taking a Table, so writing to a view doesn't compile. This is source-compatible, and the existing tests compile unchanged, but on Kotlin/Native a library compiled against an earlier version has to be recompiled. An observed query of a view watches the tables it reads. SQLite refuses an AFTER trigger on a view, so InvalidationTracker resolves it: the OpenRead instructions of EXPLAIN SELECT * FROM view name the root pages it opens, which sqlite_master maps back to tables, through views over views and joins alike, as checked with sqlite3. The view API is experimental. The generated view objects opt in, so that querying a view needs no opt-in. Tests: testView creates a view from a SELECT whose literals include 'It''s' and replace(name, "?", ""), queries it, with a WHERE on its columns, observes it through an insert into its table, and drops it. Writing to a view, CREATE on it and a SELECT of another type don't compile, and the processor's three rejections were checked with temporary files. jvmTest (57) and testAndroidHostTest on API 26 and 37 (114) pass, so EXPLAIN works on Android too. The native test sources compile for macosArm64, linuxX64, mingwX64 and watchosArm32 but were not run, as this machine is an Intel Mac. The guide to modifying the database describes views, in English and Chinese, and CREATE VIEW moves to the roadmap's Supported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
KSP 2.3.12 deprecates KSNode.validate(predicate) in favor of validate(predicate, enableNewFeatures), and the processor reported a warning for each of its two calls. They now pass enableNewFeatures = false, the replacement the deprecation names, which validates exactly as before. The processor compiles without warnings, and the code it generates for the test module compiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UByte, UShort and UInt were stored three different ways. INSERT went through their serializers, which encode the bits as a signed number, on every platform. UPDATE passed the value itself to the driver, which bound it as a signed number on the JVM, as its number on Kotlin/Native, and as its string on Android, which INTEGER affinity turned into its number. Reading truncates to the type's width, so every value read back right, but SQL saw a UByte of 200 as -56 or as 200, depending on how and where it was written: checked on the JVM, WHERE testUByte > 100 and WHERE testUInt > 2000000000 matched neither row after an INSERT or an UPDATE. Comparisons, ORDER BY, and the sum and max of such a column, which 2.4.0 typed as if it held the numbers, were wrong above the signed range, from 128 for a UByte. SQLite's 64-bit integers hold every UByte, UShort and UInt, so they are now stored as their numbers everywhere, which SQL compares, orders and sums as Kotlin does. A ULong keeps the bits of a Long, as before, since no 64-bit signed integer holds the values above Long.MAX_VALUE. - storedValue converts them; InsertValuesEncoder applies it in place of the serializers' signed encoding, as it already did for ByteArray, and SetClause applies it to what UPDATE SET binds. - The JVM driver binds them with setLong(toLong()), as the native driver does, and the Android driver converts them before execSQL, as it already did for queries, so code using sqllin-driver directly agrees across platforms too. Reading is unchanged, and reads rows written before this right. In SQL an old value above the signed range stays negative until it is written again; CHANGELOG.md gives the UPDATE that rewrites a column. testUnsignedValues inserts values above the signed range, reads them back, compares, orders, sums and takes the max of them, then updates them and compares again; it fails with the old code. The jvmTest (58) and testAndroidHostTest on API 26 and 37 (116) of sqllin-dsl-test, and those of sqllin-driver (6, 12), pass. The native test sources compile for macosArm64, linuxX64, mingwX64 and watchosArm32 but were not run, as this machine is an Intel Mac. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebuild libs/linux/libsqlite3.a and libs/windows/libsqlite3.a, which the native tests on Linux and Windows link, from the SQLite 3.51.1 amalgamation with zig, enabling FTS3, FTS4 with the enhanced query syntax, FTS5, R*Tree, Geopoly, the math functions, dbstat and column metadata. The previous libraries had only FTS5, R*Tree and the math functions, so they couldn't run tests of FTS3 and FTS4. zig cc emits debug information by default, which held paths of the machine that built the previous libraries, made them several times larger, and made the Windows one depend on the name of a temporary file. These are built with -g0, and the Windows one with its object file name set, so that they hold no paths and the same sources and options always give the same bytes. They are about a fifth of their previous size. Add two low-priority items to the roadmap: using a SQLite bundled with SQLlin on Android rather than the system's, and supporting FTS5 and R*Tree, which Android's system SQLite lacks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A @DBRow class annotated with the new @fts4 or @fts3 gets a generated FtsTable object, which CREATE creates with CREATE VIRTUAL TABLE. Its columns are the class's String properties, and a @PrimaryKey Long or Long? property named rowid or docid reads and writes the rowid. @fts4 takes the tokenizer and its arguments, the prefix lengths to index and the columns not to index, and @fts3 the tokenizer and its arguments. The processor rejects constraint annotations, columns of other types, a misnamed or mistyped rowid, and @fts4 or @fts3 without @DBRow. MATCH searches an FTS table or one of its columns, and snippet, offsets and matchinfo describe the matches. The new APIs are experimental. A virtual table can't have triggers, so an observed query of an FTS table counts its changes from SQLlin's INSERT, UPDATE and DELETE on it: the write statements now know their table, and Database#execute passes the tables a scope writes to the invalidation tracker, which keeps their counts in its log table. Which tracked tables are virtual is read again after a schema change, as a table may be created again as the other kind. withName on an FTS table returns an FTS table with the same options, so that an FTS table is rebuilt as a table is; it threw before, as it only recognized CREATE TABLE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The new repeatable annotation @check adds a CHECK constraint with an SQL expression every row has to satisfy: on a property, to its column, and on a @DBRow class, to the table. constraintName names it, which SQLite reports when a row fails it; SQLite before 3.34, as on Android below API 34, reports an unnamed one by the table's name. The annotation is experimental. SQLite gives a column's constraint the name of the named one before it, so a column's unnamed CHECK constraints come before its named ones, and all of them before the constraint of @references. The processor rejects an empty expression, a name that isn't an SQL identifier and two CHECK constraints of a table with the same name, and views and FTS tables reject @check. The generated createSQL is now escaped as a Kotlin string literal: a CHECK expression or a @default value holding a double quote broke the generated code, and one holding a dollar sign became a string template. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Triggers, upsert's DO UPDATE SET and rebuilding a table with converted data all need UPDATE's SET and INSERT to take expressions, such as visits = visits + 1. Add it to the medium-priority items, before CREATE TRIGGER, which needs it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
select AS SomeView makes a SELECT a derived table, (SELECT ...) AS name, named after the object generated for a @dbview class, which gives it its name and column properties, though no such view has to exist in the database. It is read like a table in the FROM clause and in joins, and a derived table of a table's rows joins the table with itself. IN takes a SELECT of a single column of values of the same kind, which is checked when the statement is built, and EXISTS(select) checks whether a SELECT returns any row, referring to the columns of the query it is in if need be. NOT(condition) negates a condition in parentheses, for NOT EXISTS and NOT IN. These APIs are experimental. A relation now gives how a FROM clause writes it, the parameters that writes, and the tables it reads: a statement binds the parameters of its derived tables first, then those of its clauses, always into a new list, so that a derived table read by several queries keeps its own, and a condition carries the tables of its subqueries to the observed queries. A WHERE after result columns no longer drops the parameters before it, which had none until now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The infix AND and OR apply in the order they are written, as Kotlin's infix functions do, so (age EQ 1) OR (age EQ 2) AND (name EQ "x") means (age = 1 OR age = 2) AND name = 'x'. They combined the SQL without parentheses, as age = 1 OR age = 2 AND name = 'x', which SQLite reads as age = 1 OR (age = 2 AND name = 'x'), as SQL gives AND precedence over OR, so the query returned other rows, though the KDoc promised parentheses. A condition now knows the operator it combines others with, and is put in parentheses as an operand of the other one, while a chain of the same operator is written as it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Combining a condition with AND or OR added the other condition's parameters to its own list, so a condition kept in a variable and used again, alone or combined, bound parameters its SQL doesn't have, which failed with an ArrayIndexOutOfBoundsException on the JVM. Combining conditions now puts their parameters in a new list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SQLite SQLlin runs on depends on the platform: Android's system SQLite is 3.9 at API 24, SQLlin's minimum, and lacks options such as the math functions, Apple's depends on the OS version, and on Linux and Windows it is the SQLite the app links. An API that needs a newer SQLite or a compile-time option fails at runtime where it is missing, so the new opt-in annotation @PlatformDependentSQLiteAPI marks such APIs, and using one is a compile error unless the app opts in. RENAME_COLUMN, which needs SQLite 3.25.0 (Android API 30), and DROP_COLUMN, which needs SQLite 3.35.0 (Android API 34), now carry it, which is a breaking change for code using them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ER BY A column given to a function, to GROUP BY or to ORDER BY, or selected into a property with AS, was written without its table's name, as in count(name), so in a join of tables that both have a column of that name, SQLite failed with "ambiguous column name". Conditions already wrote columns with their tables' names; now these do too, as in count(book.name), except in the ORDER BY of a compound SELECT, which can only name the columns of its results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FROM(relation) starts a join, which INNER_JOIN, LEFT_OUTER_JOIN, RIGHT_OUTER_JOIN, FULL_OUTER_JOIN, CROSS_JOIN and the NATURAL joins extend, each with its own ON or USING, so that a join can have more than two relations. A join is selected from with a projection, a clause function such as WHERE<R>(...), or result columns, which could only select from a single table until now. The new APIs are experimental, and RIGHT and FULL OUTER JOIN, which need SQLite 3.39.0, are marked with @PlatformDependentSQLiteAPI. The type rows are read into is checked against the joined relations when the statement is built: every property has to name a column of one of them, of a single one unless USING or a NATURAL join merges them, and has to be nullable where an outer join can leave its column or expression NULL, which elements now know from the tables of the columns they read. A relation can only be joined once. What a SELECT reads is now an internal From, a relation or a join, which these checks and the SQL of the columns work on; a single relation keeps its SQL and messages. A join doesn't start with its first relation as the receiver, as in PersonTable INNER_JOIN BookTable, as such an infix function would take over the old CROSS_JOIN(CategoryTable) in the lambda of a table, whose receiver is that table. The old joins, written as clauses of SELECT, are documented to be removed in the next version after 2.5.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since columns are written with their tables' names, a function given to CREATE_INDEX or CREATE_UNIQUE_INDEX, as lower(name), became lower(person.name), and the index took what followed its last '.', so it wrote name), a syntax error, where it had made an expression index. SQLite doesn't allow '.' in the expressions of an index anyway. An index now writes a column by its name, and a function without the indexed table's name before its columns, outside its string literals. A column or function of another table is rejected, and the KDoc documents expression indexes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the SQLite syntax that SQLlin doesn't support yet and the roadmap didn't list, from DROP INDEX to window functions: the items that are both easy and useful as high priority, those that are one of them as medium priority, and the others, especially those that need much change or a design discussion, as low priority. Add the removal of the old joins in the next version after 2.5.0 too. Give every item an ID, Fx, which stays the same when it moves to another priority or is done, and which a new item takes the next number of. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Database created its InvalidationTracker at once, and every scope it ran called the tracker's refresh, so the tracker's code was reachable from any app, which code shrinkers then kept, with the coroutines code its StateFlows need, though only observed queries use it. The first observed query now creates it, under the executive mutex, and a scope refreshes it only once it exists. On Kotlin/Native, a release executable that only inserts, queries, updates and deletes went from 1,752,352 to 1,333,936 bytes, and with R8, the code of sqllin-dsl it keeps went from 44 KB to 33 KB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing dropped the indexes that CREATE_INDEX and CREATE_UNIQUE_INDEX create. DROP_INDEX(name) does, with DROP INDEX, as an experimental API. The guide to modifying the database now describes indexes, ordinary, unique and expression ones, which only the KDoc did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CREATE_IF_NOT_EXISTS, CREATE_INDEX_IF_NOT_EXISTS, CREATE_UNIQUE_INDEX_IF_NOT_EXISTS and CREATE_VIEW_IF_NOT_EXISTS do nothing when what they create exists, and DROP_IF_EXISTS, of a table or a view, and DROP_INDEX_IF_EXISTS when what they drop doesn't, so that a migration step can run more than once. CREATE_IF_NOT_EXISTS creates FTS tables too, as CREATE VIRTUAL TABLE takes IF NOT EXISTS. They are experimental. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WHERE after CREATE_INDEX, CREATE_UNIQUE_INDEX or their IF_NOT_EXISTS forms makes an index of the rows that satisfy a condition, such as a unique index of the emails of the users that aren't deleted. SQLite allows neither parameters nor subqueries there, so the values of the condition are written into the SQL, its columns without their table's name, and a subquery is rejected. WHERE is experimental. For WHERE to complete it, these functions return an IndexStatement, where they returned Unit, which replaces the statement where it is among those of the scope or the transaction. This is source-compatible, but a library compiled against an earlier version has to be recompiled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DISTINCT(element) makes count, avg, sum and group_concat take only the distinct values of an element, as count(DISTINCT book.author) does. It returns a Distinct, which only these functions take, so it doesn't compile anywhere else, where SQL doesn't allow it. max and min don't take it, as it doesn't change them, and group_concat separates the distinct values with commas, as SQLite allows only one argument with DISTINCT. It is experimental. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ESCAPE after LIKE gives the pattern an escape character, after which %, _ and the escape character match only themselves, as in name LIKE "100\\%%" ESCAPE '\\', which matches the names that start with 100%. It is experimental. LIKE now returns a LikeCondition, a SelectCondition that ESCAPE can follow, so that it reads as SQL does, without parentheses, and doesn't compile after any other condition. This is source-compatible, but a library compiled against an earlier version has to be recompiled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VACUUM() rebuilds the database file into the minimum disk space, as deleting rows doesn't shrink it, and VACUUM_INTO(file) writes a vacuumed copy of the database to another file, a consistent backup of a database in use. Both throw inside a transaction, where SQLite can't vacuum, and are experimental. VACUUM INTO needs SQLite 3.27.0, Android API 30, so it is marked with @PlatformDependentSQLiteAPI. As a change of the schema does, VACUUM makes observed queries query again, as it may change the rowids of the tables without an INTEGER PRIMARY KEY. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reading a column's property in SET {}, as in SET { visits = visits + 1 },
compiled, but its getter only returns a placeholder, as 0 or "", so the
column was set to a value computed from it, here 1, with no error.
The processor now generates the getter deprecated with the level ERROR,
so reading the property doesn't compile, while assigning it is
unchanged. The test of the generated properties now assigns them, and
the compile-time check of their nullability, which reads their types,
suppresses the error. This is a breaking change for code that reads
them, which only got placeholders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guide to rebuilding a table renamed the new table after dropping the old one, which fails while a view reads the dropped table on the JVM, Linux and Windows, where SQLite's legacy_alter_table is off by default, as renaming then checks every view: "error in view ...: no such table". Views came with 2.5.0. The guide now drops such views before dropping the old table and creates them again after the rename, which works on every platform, and says that only where the setting is off do the foreign keys of other tables follow a renamed table whether or not foreign keys are enforced. testTableRebuild rebuilds a table a view reads, which fails on the JVM without dropping the view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first version of triggers leaves out INSTEAD OF triggers, which only views can have, and which would be of no use while the DSL can't write to a view, as a view isn't a Table. F63 keeps them for later, together with INSERT, UPDATE and DELETE on views. Supported now lists the items in the order they were done, the newest first, so F1 to F6 are put in that order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An element now knows the tables its subqueries read, which an observed query watches, whether it always gives the same result for a row, and whether it is a literal on its own, and an expression derives these from its operands. An expression carries no parameters: its values are written into its SQL, so that it can be part of a view, a trigger or an index. literal(value) makes a value an expression; +, -, *, / and the unary - compute with numbers of the same type, % with integers, and + of strings concatenates them; CAST(x AS INT) converts an expression to the types SQLlin names columns with. The new functions take expressions as their arguments: coalesce, ifnull and nullif, the scalar max and min, hex, quote, unicode, randomblob, zeroblob, more forms of trim, substr, replace, instr, printf and round, the aggregate total, and the date and time functions. sign, unixepoch and the math functions, which need a newer SQLite or its math functions, are marked with @PlatformDependentSQLiteAPI. A function can only be NULL where its result can, as a division by zero is. ORDER BY and GROUP BY reject a literal on its own, which SQLite would read as the number of a result column, and an index rejects an expression with a subquery, an aggregate function, or a function that doesn't always give the same result, which SQLite only rejects once the table has rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CASE(WHEN(condition) THEN value, ..., ELSE = value) is SQL's searched CASE, which is NULL where ELSE is omitted. select[R::property] uses a SELECT of a single column as a value, which is checked when the statement is built, and whose tables an observed query watches. IS and ISNOT of two expressions compare them as EQ and NEQ do, NULL equaling NULL, which IS DISTINCT FROM does on newer SQLite only. COLLATE compares and sorts a string by BINARY, NOCASE or RTRIM, and and, or, inv(), shl and shr are the bitwise operators of Int and Long. JSON's -> and ->>, which need SQLite 3.38.0, become their own item of the roadmap, F64, and REGEXP is left out, as the JVM's SQLite has no such function, and Android's only matches whole strings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SET(listOf(expression AS Row::property)) sets columns to expressions,
as visits + 1, and INSERT, INSERT_OR_IGNORE and INSERT_OR_REPLACE take
a row of expressions, for INSERT INTO ... VALUES, leaving the other
columns to their default values. AS checks the type of each expression
at compile time, and the statement checks, when it is built, the
checks of result columns that apply: an expression that can be NULL is
only written to a nullable column, and it isn't an aggregate function
and only reads the updated table, or no table in INSERT, where SQLite
allows none. The error of reading a column in SET {} now points to
these.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CREATE_TRIGGER(name) AFTER UPDATE_OF(column) ON table WHEN (condition)
BEGIN { ... } creates a trigger, with BEFORE and AFTER, the events
INSERT, DELETE, UPDATE and UPDATE_OF, and an IF NOT EXISTS form, and
DROP_TRIGGER drops one. new(column) and old(column) read the row being
written, and a row the trigger doesn't have, which SQLite would only
report when the trigger fires, is rejected when the statement is built.
The body is written as any statements are: the scope collects them in
its own container instead of running them, and only INSERT, UPDATE,
DELETE and RAISE are allowed. The values of a trigger are written into
its SQL, as SQLite doesn't let a trigger take parameters.
A trigger can write an FTS table, which the tracker of observed queries
can't count changes of with triggers, so a write to a table that has
triggers now counts as a change of every tracked FTS table. The guide
to rebuilding a table drops the triggers of other tables that read it,
as it does views, and creates the table's own again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
table INSERT DEFAULT_VALUES inserts a row of the default values of all columns, as INSERT INTO ... DEFAULT VALUES does, with INSERT_OR_IGNORE and INSERT_OR_REPLACE too. A column without a default value takes NULL, which a NOT NULL one rejects. DEFAULT_VALUES is a property of the scope, as the events of triggers are, so it needs no import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ANALYZE gathers the statistics the query planner chooses indexes with, of the whole database, a table or an index, and REINDEX rebuilds the indexes of the database, a table, an index or a collation. PRAGMA optimize, which needs SQLite 3.18.0, Android API 26, is marked with @PlatformDependentSQLiteAPI. Like VACUUM, they count as changes of the schema for observed queries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CREATE_INDEX, CREATE_UNIQUE_INDEX and their IF_NOT_EXISTS forms take
the columns with the orders they are sorted in, as ORDER_BY does:
CREATE_INDEX("idx_person_age", PersonTable.age to DESC). The first
column is a parameter of its own, so that a call without columns, which
fails, still has a single candidate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ORDER_BY(age to ASC NULLS LAST) puts the NULL values last, as SQL reads, and DESC NULLS FIRST first, which need SQLite 3.30.0, Android API 31, so NULLS is marked with @PlatformDependentSQLiteAPI. ORDER BY now takes an Ordering, which OrderByWay and the new NullsOrdering are, rather than an OrderByWay. Pair and Map are covariant, so this is source-compatible, but a library compiled against an earlier version has to be recompiled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A COMMIT that fails, as one does while a deferred foreign key isn't satisfied, leaves SQLite's transaction open. No driver rolled it back, so the statements after it ran in it, and the next transaction failed to begin, with "cannot start a transaction within a transaction". The JVM driver even set the connection back to auto-commit over the open transaction. The JVM and Kotlin/Native drivers now roll it back, and throw the error of the COMMIT. Android's framework lets go of such a transaction, and turns a ROLLBACK given to it into its own end of a transaction, which it has none of: it tells a ROLLBACK by its first characters, after a comment too from some API level on. So the Android driver raises the ROLLBACK with a TEMP trigger, whose table and trigger, created in the transaction, are rolled back with it, as tested on API 26 and 37. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
deferred = true on @references and @ForeignKeyGroup makes a foreign key DEFERRABLE INITIALLY DEFERRED: checked when the transaction commits, rather than after each statement, so that a transaction can insert a child before its parent, or rows that refer to each other. A transaction that commits while such a key isn't satisfied fails and is rolled back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The onConflict of @PrimaryKey, @CompositePrimaryKey, @unique and @CompositeUnique, of the new experimental enum OnConflict, writes the ON CONFLICT clause of the constraint, as UNIQUE ON CONFLICT REPLACE. ABORT, the default, writes none, so the tables declared before are created as they were. A composite key or group takes the one given on any of its properties, and the processor rejects one whose properties give different ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F17 gave PRIMARY KEY and UNIQUE their conflict clauses, but not NOT NULL, which a property's type declares, so that no annotation can hold its clause yet. It would let NOT NULL ON CONFLICT REPLACE write the default value instead of NULL, which SQLlin's statements rarely write to a non-null column. F65 keeps it, with the two ways it could be declared, until other syntax makes it of more use, or shows it isn't needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New features for 2.5.0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
All
Kotlin's version to2.4.21libs, which the native tests on Linux and Windows link, from SQLite 3.51.1 with FTS3, FTS4 (with the enhanced query syntax), FTS5, R*Tree, Geopoly, the math functions,dbstatand column metadata enabled, and without debug information, which held paths of the machine that built themsqllin-dsl
@PlatformDependentSQLiteAPI, which marks the APIs that only work with some SQLite versions or compile-time options, which some platforms SQLlin supports don't have, such as Android's system SQLite at lower API levels. Using one is a compile error unless it is accepted with@OptIn(PlatformDependentSQLiteAPI::class)RENAME_COLUMN, which needs SQLite 3.25.0 (Android API 30), andDROP_COLUMN, which needs SQLite 3.35.0 (Android API 34), are marked with@PlatformDependentSQLiteAPI, so code using them has to opt inFROM(relation)starts a join, whichINNER_JOIN,LEFT_OUTER_JOIN,RIGHT_OUTER_JOIN,FULL_OUTER_JOIN,CROSS_JOINand the NATURAL joins extend, each with its ownONorUSING, as in(FROM(PersonTable) LEFT_OUTER_JOIN BookTable ON (PersonTable.id EQ BookTable.authorId)) SELECT listOf(...). A join is selected from with a projection, a clause function such asWHERE<R>(...), or result columns, which can now select from a join, and the type its rows are read into is checked against the joined relations when the statement is built: every property has to name a column of one of them, of a single one unless USING or a NATURAL join merges them, and has to be nullable where an outer join can leave its column or expression NULL. Columns are written with their relations' names, and an observed query of a join watches all its relations.RIGHT_OUTER_JOIN,FULL_OUTER_JOINand their NATURAL forms need SQLite 3.39.0 (Android API 34), so they are marked with@PlatformDependentSQLiteAPItable SELECT INNER_JOIN<R>(other) ON (...), which don't check the type their rows are read into, will be removed in the next version after 2.5.0: use the joins of relations insteadCREATE_INDEXandCREATE_UNIQUE_INDEXdocument that a function of the table's columns makes an expression index, aslower(email)for case-insensitive lookups or uniqueness, and reject a column or function of another tableIF NOT EXISTSandIF EXISTS, withCREATE_IF_NOT_EXISTS,CREATE_INDEX_IF_NOT_EXISTS,CREATE_UNIQUE_INDEX_IF_NOT_EXISTS,CREATE_VIEW_IF_NOT_EXISTS,DROP_IF_EXISTSandDROP_INDEX_IF_EXISTS, which do nothing when the table, FTS table, index or view exists, or doesn't, so that a migration step can run more than onceDROP_INDEX(name), for SQL syntaxDROP INDEX, which drops an index thatCREATE_INDEXorCREATE_UNIQUE_INDEXcreated. The guide to modifying the database now describes indexesWHEREafterCREATE_INDEX,CREATE_UNIQUE_INDEXor theirIF_NOT_EXISTSforms indexes only the rows that satisfy a condition, as inUserTable.CREATE_UNIQUE_INDEX("idx_user_email", UserTable.email) WHERE (UserTable.isDeleted IS false). SQLite allows neither parameters nor subqueries there, so the values of the condition are written into the SQL, and a subquery is rejected with anIllegalArgumentException. To support it, these functions now return anIndexStatement, where they returnedUnit; this is source-compatible, but a library compiled against an earlier version has to be recompiledDISTINCT(element), for aggregate functions of the distinct values, as incount(DISTINCT(author))forcount(DISTINCT book.author).count,avg,sumandgroup_concattake it, andgroup_concatseparates the distinct values with commas, as SQLite allows no other separator with DISTINCT. What it returns, aDistinct, can't be used anywhere else, which is checked at compile timeESCAPEafterLIKE, for SQL syntaxLIKE pattern ESCAPE character, as inname LIKE "100\\%%" ESCAPE '\\', after which%,_and the escape character match only themselves. To support it,LIKEnow returns aLikeCondition, which is aSelectCondition; this is source-compatible, but a library compiled against an earlier version has to be recompiledVACUUM()andVACUUM_INTO(file), for SQL syntaxVACUUMandVACUUM INTO, which rebuild the database file into the minimum disk space, or write a vacuumed copy of the database to another file, a consistent backup of a database in use. Both throw anIllegalStateExceptioninside a transaction, where SQLite can't vacuum.VACUUM_INTOneeds SQLite 3.27.0 (Android API 30), so it is marked with@PlatformDependentSQLiteAPI+,-,*,/, the unary-and, of integer types,%compute with numbers of the same type, and with values of their type, as inpages + 1, giving a number of that type, and+concatenates strings, SQL's||. They work in result columns, conditions and ORDER BY. SQLite gives NULL for a division by zero, so/and%can be NULL unless the divisor is a value other than zero.literal(value)makes a value an expression, andCAST(x AS type)converts an expression to another type, named as SQLlin names the columns of that Kotlin type:TINYINT,SMALLINT,INT,BIGINT,FLOAT,DOUBLE,TEXTandBLOB. The values in an expression are written into its SQL, so that it can be part of a view, a trigger or an index, which SQLite doesn't let take parameters. A literal on its own in ORDER BY or GROUP BY is rejected, as SQLite would read it as the number of a result columncoalesce,ifnullandnullif, the scalarmaxandminof several expressions,hex,quote,unicode,randomblob,zeroblob,trim,ltrimandrtrimwith the characters to remove,substrfrom a position,printfof several values,substr,replaceandinstrof expressions,roundto an integer, the aggregatetotal, and the date and time functionsdate,time,datetime,juliandayandstrftime. A function can only be NULL where its result can:coalescewhere all its arguments can, and a date and time function always, butdatetime("now").sign(SQLite 3.35.0, Android API 34),unixepoch(SQLite 3.38.0, Android API 34) and the math functionsceil,floor,trunc,sqrt,exp,ln,log10,log2,pow,modandpi, which Android's SQLite has at no API level, are marked with@PlatformDependentSQLiteAPICASE(WHEN(condition) THEN value, ..., ELSE = value), SQL's searched CASE, which is NULL whereELSEis omitted; scalar subqueries,select[R::property], a SELECT of a single column used as a value, which is checked when the statement is built, and whose tables an observed query watches;ISandISNOTof two expressions, the null-safe comparisons, which do whatIS DISTINCT FROMdoes on every SQLite version; the COLLATE operator with the collationsBINARY,NOCASEandRTRIM; and the bitwise operatorsand,or,inv(),shlandshrofIntandLongSET(listOf(expression AS Row::property))sets columns to expressions, as intable UPDATE SET(listOf((visits + 1) AS Person::visits)) WHERE (...), andINSERT,INSERT_OR_IGNOREandINSERT_OR_REPLACEtake a row of expressions,table INSERT listOf(literal("purge") AS Audit::action, datetime("now") AS Audit::at), for SQL syntaxINSERT INTO ... VALUES, leaving the other columns to their default values or NULL.ASchecks the type of each expression at compile time, and the statement checks, when it is built, that an expression that can be NULL is only written to a nullable column, that it isn't an aggregate function, and that it only reads the columns of the updated table, or none for INSERT, where SQLite allows noneCREATE_TRIGGER(name) AFTER DELETE ON PersonTable WHEN (condition) BEGIN { ... }, for SQL syntaxCREATE TRIGGER, withBEFOREandAFTER, the eventsINSERT,DELETE,UPDATEandUPDATE_OF(columns), andCREATE_TRIGGER_IF_NOT_EXISTS.new(column)andold(column)read the row being written, and a row the trigger doesn't have, which SQLite would only report when the trigger fires, is rejected when the statement is built. The body inserts, updates and deletes rows, with expressions, andRAISE(ABORT, message),RAISE(FAIL, message),RAISE(ROLLBACK, message)andRAISE(IGNORE)stop the statement that fired the trigger or skip its row. The values of a trigger are written into its SQL, as SQLite doesn't let a trigger take parameters. A trigger can't be on an FTS table or a view; INSTEAD OF triggers on views are left to another item of the roadmap.DROP_TRIGGER(name)andDROP_TRIGGER_IF_EXISTS(name)drop one. An observed query of an FTS table now counts the writes to any table that has triggers as changes of the FTS table too, as a trigger can write ittable INSERT DEFAULT_VALUES, for SQL syntaxINSERT INTO ... DEFAULT VALUES, which inserts a row of the default values of all columns, withINSERT_OR_IGNOREandINSERT_OR_REPLACEtooANALYZE(),ANALYZE(table)andANALYZE(name), for SQL syntaxANALYZE, which gathers the statistics the query planner chooses indexes with;REINDEX(),REINDEX(table),REINDEX(name)andREINDEX(collation), forREINDEX, which rebuilds indexes; andPRAGMA_OPTIMIZE(), forPRAGMA optimize, which needs SQLite 3.18.0 (Android API 26), so it is marked with@PlatformDependentSQLiteAPIPersonTable.CREATE_INDEX("idx_person_age", PersonTable.age to DESC), withCREATE_UNIQUE_INDEXand theIF_NOT_EXISTSforms tooNULLS FIRSTandNULLS LASTin ORDER BY, as inORDER_BY(age to ASC NULLS LAST), which need SQLite 3.30.0 (Android API 31), so they are marked with@PlatformDependentSQLiteAPI. To support them, ORDER BY takes anOrdering, whichOrderByWayis, rather than anOrderByWay; this is source-compatible, but a library compiled against an earlier version has to be recompileddeferred = trueon@Referencesand@ForeignKeyGroup, which makes a foreign keyDEFERRABLE INITIALLY DEFERRED: checked when the transaction commits, rather than after each statementonConflictof@PrimaryKey,@CompositePrimaryKey,@Uniqueand@CompositeUnique, with the enumOnConflict, for theON CONFLICTclause of the constraint, as inUNIQUE ON CONFLICT REPLACE. A composite key or group takes the one given on any of its properties, and the processor rejects a key or group whose properties give different ones. ABORT, the default, writes no clauseCREATE_INDEXandCREATE_UNIQUE_INDEXreject an expression with a subquery or an aggregate function, or a function that doesn't always give the same result for a row, such asrandom(),datetime('now')or a date and time function with thelocaltimemodifier, and the WHERE of a partial index rejects such a function, where SQLite would only fail once the table has rowsselect AS SomeViewmakes a SELECT a derived table,(SELECT ...) AS name, named after the object generated for a@DBViewclass, which gives it its name and column properties, though no such view has to exist in the database. A derived table is read like a table, in the FROM clause and in joins, which can join a table with itself through a derived table of its rows.INtakes a SELECT of a single column of values of the same kind, which is checked when the statement is built, andEXISTS(select)checks whether a SELECT returns any row; it can refer to the columns of the query it is in. They also work in the WHERE of UPDATE and DELETE. Parameters are bound in the order the SQL has them, and an observed query watches the tables its subqueries readNOT(condition), which negates a condition in parentheses, as inNOT(EXISTS(select))@Check, which adds aCHECKconstraint with an SQL expression every row has to satisfy: on a property, to its column, and on a@DBRowclass, to the table.constraintNamenames it, which SQLite reports when a row fails it; SQLite before 3.34, as on Android below API 34, reports an unnamed one by the table's name. The processor rejects an empty expression, a name that isn't an SQL identifier, and two constraints of a table with the same name, and views and FTS tables reject@Check@DBRowclass annotated with@Fts4or@Fts3gets a generatedFtsTableobject, whichCREATEcreates withCREATE VIRTUAL TABLE. Its columns are the class'sStringproperties, and a@PrimaryKeyproperty of typeLongorLong?namedrowidordocidreads and writes the rowid.@Fts4takes the tokenizer and its arguments, the prefix lengths to index and the columns not to index, and@Fts3the tokenizer and its arguments.MATCHsearches the table or one of its columns, andsnippet,offsetsandmatchinfodescribe the matches. An observed query of an FTS table counts its changes from SQLlin's INSERT, UPDATE and DELETE on it, as a virtual table can't have triggers.withNameon an FTS table returns an FTS table, so it is rebuilt as a table is@DBViewgets a generated view object, such asAdultViewforAdult, whichCREATE_VIEW(AdultView) AS (select)creates from a SELECT of its row type, checked at compile time, andDROP(AdultView)drops. SQLite doesn't let a view take parameters, so the SELECT's values are written into the view's SQL. A view is queried like a table, in joins and compound SELECTs too, and an observed query of it watches the tables it reads, which SQLite lists throughEXPLAIN. To keep views from being written to, the newRelationis the base ofTableandView: SELECT, joins, the SQL functions and the clause elements take aRelation, while INSERT, UPDATE, DELETE, CREATE and ALTER keep taking aTable. This is source-compatible, but on Kotlin/Native a library compiled against an earlier version has to be recompiledDatabase#observe, which turns a SELECT into aFlowof its results, emitted when it is collected and again whenever a statement run through the database changes a table the SELECT reads, if the results differ. A join or a compound SELECT watches all of its tables. The changes are counted by SQLite itself, through TEMP triggers on the observed tables, so rows changed by foreign key actions count, while rolled back transactions and statements that match no rows don't. Changes made through anotherDatabaseinstance, connection or process aren't seen. Using it needs@OptIn(ExperimentalDSLDatabaseAPI::class)kotlinx.coroutinesis now anapidependency ofsqllin-dsl, asDatabase#observereturns aFlowDatabasecreates the tracker of observed queries when it observes its first query, so that an app that observes none doesn't reach its code, which code shrinkers then remove: on Kotlin/Native, a release executable of an app that only inserts, queries, updates and deletes is about 418 KB smaller, and with R8, the code ofsqllin-dslit keeps is a quarter smallerUNION,UNION_ALL,INTERSECTandEXCEPTas infix operators between two SELECTs of the same result type, as in(table SELECT WHERE(age LT 18)) UNION (table SELECT WHERE(age GTE 65)). The result type is checked at compile time, so the SELECTs can combine the rows of different tables through projections, result columns or joins. Chained operators are evaluated from left to right, as in SQL, and parentheses group them. A SELECT that ends with ORDER BY or LIMIT keeps them to itself, and the compound can be followed by its own ORDER BY and LIMIT. Using it needs@OptIn(ExperimentalDSLDatabaseAPI::class)DSLDBConfiguration,Database(DSLDBConfiguration)and the DSL APIsCREATE,CREATE_INDEX,CREATE_UNIQUE_INDEX,DROP,ALTER_ADD_COLUMN,ALTER_RENAME_TABLE_TO,RENAME_COLUMN,DROP_COLUMNandPRAGMA_FOREIGN_KEYSare no longer experimental, so using them no longer needs@OptIn(ExperimentalDSLDatabaseAPI::class)UNION {}andUNION_ALL {}blocks, and theDatabaseScope#beginUnion,createUnionSelectStatementandendUnionfunctions they use. A block returns the table's row type but reads the rows into its first SELECT's result type, so a union of projections, result columns or joins fails with aClassCastExceptionwhen its results are used, and a nested block is flattened into the SQL, which changes its meaning unless it is the block's first statement. These bugs won't be fixed: use the infix operators instead. The blocks will be removed in a future versionlegacy_alter_tablesetting is off by default, renaming the new table failed while a view read the dropped one, with "error in view ...: no such table". The guide now drops the views before dropping the old table and creates them again after the rename, which works on every platform, and explains that only where the setting is off do the foreign keys of other tables follow a renamed table regardless of whether foreign keys are enforcedAS, was written without its table's name, so in a join of tables that both have a column of that name, SQLite failed with "ambiguous column name". Such columns are now written with their tables' names, as conditions already were, except in the ORDER BY of a compound SELECT, which can only name the columns of its results(age EQ 1) OR (age EQ 2) AND (name EQ "x")means(age = 1 OR age = 2) AND name = 'x', but SQL gives AND precedence over OR, so SQLite read it asage = 1 OR (age = 2 AND name = 'x')and returned other rows. A condition that combines others with the other operator is now put in parenthesesArrayIndexOutOfBoundsExceptionon the JVM. Combining conditions now leaves their parameters as they wereUByte,UShortorUIntas the bits of a signed number on every platform, UPDATE did so on the JVM, but stored it as its number on Kotlin/Native and Android, so aUByteof 200 read back as 200, but compared, ordered and summed as -56 or as 200, depending on how it was written. They are now always stored as their numbers, which SQLite's 64-bit integers hold, so that SQL compares, orders and sums them as Kotlin does, assumandmaxof such a column assumed. AULongkeeps the bits of aLong, as no 64-bit signed integer holds the values aboveLong.MAX_VALUE. Reading is unchanged and reads the old rows right, but in SQL an old value above the signed range stays negative until it is written again, which one UPDATE per column does:UPDATE t SET c = c + 256 WHERE c < 0for aUBytecolumn, with65536for aUShortone and4294967296for aUIntonesqllin-driver
UInt,UShortorUByteparameter as the bits of a signed number, unlike the Kotlin/Native driver, and the Android driver bound one written throughexecSQLas its string. All of them now bind it as its number, and aULongas theLongof the same bitssqllin-processor
createSQLof a table didn't escape",\and$, which a Kotlin string literal treats specially, so a@Defaultvalue holding one could make the generated code fail to compile, or hold another valueSET {}, as inSET { visits = visits + 1 }, compiled, but its getter only returns a placeholder, such as0or"", so the column was set to a value computed from it, here1. The getter is now deprecated with the levelERROR, so reading the property is a compile error, while assigning it is unchanged🤖 Generated with Claude Code