-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Description
Currently, EF by convention configures integer primary keys to use the SQLite AUTOINCREMENT feature:
CREATE TABLE "Blogs" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Blogs" PRIMARY KEY AUTOINCREMENT,
"Name" TEXT NOT NULL
);
This mirrors how we configure IDENTITY by default for integer primary keys on SQL Server. However, unlike IDENTITY in SQL Server, the SQLite docs explicitly discourage the use of AUTOINCREMENT for most purposes, pointing out that it "imposes extra CPU, memory, disk space, and disk I/O overhead". SQLite has another default (and recommended) autoincrement algorithm using rowid, which is used by default for INTEGER PRIMARY KEY
(docs):
create table foo (id INTEGER PRIMARY KEY, data text);
insert into foo (data) VALUES ('foo'), ('bar');
select id from foo;
1
2
The original reasoning for doing AUTOINCREMENT by default can be found in #2432: the default algorithm (without AUTOINCREMENT) reuses values from deleted rows, which can lead to unexpected behaviors in some scenarios. I believe that this may have been a bad decision:
- EF should not be second-guessing the database and overriding its defaults.
- The scenarios where the default algorithm leads to unexpected behaviors are IMHO contrived.
- We should be providing optimal perf by default, as these kinds of nuances are very hard for users to discover, and almost all will be on the default sub-optimal performance without ever making a decision that they actually need AUTOINCREMENT.
Regardless and in addition to the above, it is currently impossible to disable database key generation. The usual way to do this in EF is the following:
modelBuilder.Entity<Blog>()
.Property(b => b.Id)
.ValueGeneratedNever();
While this indeed disables AUTOINCREMENT, the SQLite table created still uses the default rowid algorithm:
CREATE TABLE "Blogs" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Blogs" PRIMARY KEY,
"Name" TEXT NOT NULL
);
Since ValueGeneratedNever() configures EF to treat the column as non-value-generated, EF users will mostly get the expected behavior, as EF will always explicitly send the Id value to the database, overriding the value generation. However, the table definition is incorrect, and users outside of EF will continue to see value generation happening.
When the user configures the primary key as non-generated, we should consider creating the table with WITHOUT ROWID
(docs); this is the recommended practice in the docs, with the default CREATE TABLE - with rowids - described as legacy (see quirks). WITHOUT ROWID
can have space and performance advantages compared to regular tables.
On the other hand, WITHOUT ROWID
also causes some behavioral changes and disables certain features (docs). To preserve backwards compatibility while still disabling value generation, we can generate INT PRIMARY KEY
columns instead of INTEGER PRIMARY KEY
: this keeps the rowid (thus preserving backwards compat) while making the Id a regular, non-generated column (this type quirk is documented).
To summarize, I'm proposing that we extend the recently-introduced SqliteValueGenerationStrategy as follows:
- Autoincrement: already exists today, creates AUTOINCREMENT primary keys.
- Rowid: creates a regular/default table (with rowids), without AUTOINCREMENT (
INTEGER PRIMARY KEY
). This correspond to the current behavior of None. - NoRowid: creates a table with
WITHOUT ROWID
. - None: creates a regular/default table (with rowids), but with
INT PRIMARY KEY
to disable value generation on the key column. Note that this changes the meaning of None (from Rowid currently toINT PRIMARY KEY
), but is a bug fix rather than a breaking change.
We can also consider doing the following breaking changes (but we don't have to):
- When the primary key is numeric and value-generated, switch the default from Autoincrement to Rowid. This is the recommended default in the SQLite docs, suitable for most scenarios, and provides optimal performance out-of-the-box.
- When the primary key is numeric and non-value-generated, switch the default from today's None (but which actually does configure value generation, corresponding to the new RowId behavior) to the new NoRowid (as recommended by the SQLite docs). This This is the recommended default in the SQLite docs, suitable for most scenarios, and provides optimal performance out-of-the-box.
Note that while the above are breaking changes, they would only affect new SQLite tables (as existing tables have already been created and would presumably not be altered). This reduces the blast radius of the breakage somewhat.
As a side-note, the above value generation is only supported on the primary key (we should verify that this is validated etc.).