redb 4.0.0 #13
reliktbk
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
redb4.0.0 is out. Two things before the feature list. Pro stays free on the whole 4.x line, which is the opposite of what the 3.7.0 note said a major would do. And one mechanism is gone: Props-level lazy loading, replaced by lazy references.This is the V4 release. The theme running through it is uniqueness enforced by the database rather than by application code, and a storage layer that behaves when the application does not own its database.
Pro is free on 4.x, no key
LicensePolicy.FreeThroughMajormoved from 3 to 4. The 3.7.0 announcement explained that a public API removal was held out of a major because a major would flip that constant and start charging for Pro. The constant moved with the major instead, as promised in the answer to discussion #10: Pro is free in 4, no key, commercial use included. Every Pro package on the 4.x line runs unrestricted.Unique keys
Uniqueness used to be an application concern. It now lives in the storage, on five levels, each enforced by a partial unique index and surfacing one typed
RedbUniqueViolationExceptionon every provider instead of three driver errors._objects._value_unique. A plain readable base field the application fills itself, unique per scheme:obj.ValueUnique = "ORD-2026-0001". It behaves like any other base field in JSON projections, LINQ, prefix search, bulk writers and export/import.SaveByUniqueAsyncis the upsert by that key, and it survives a lost creation race.[RedbUnique]on a Props field, a root scalar. The canonical form of the value (NFC, no trimming, no case folding, UTC milliseconds,G29for decimals, a column tag for cross-column injectivity) is hashed into_values._unique. Point lookup isGetByUniqueAsync<TProps>(p => p.Code, value), one probe of the index.[RedbUnique]on a scalar inside a nested class, keyed with its path, so two nested classes cannot collide.[RedbUnique]on a whole nested class, array or dictionary, a subtree key. The key is the content: a dictionary is order-insensitive, a list is ordered, references canonicalise by id. "Unique" reads as "no two objects of this scheme hold this exact content".[RedbUnique(Scope = ...)]on collection elements.Scope = Collectionmeans no duplicate elements inside one object's collection;Scope = Schememeans element values are unique across every object of the scheme, probeable withGetByUniqueAsync(p => p.Emails, value).A misplaced attribute is refused at scheme synchronisation, not ignored until an insert. An attribute added to a structure that already holds data recomputes the column and reports the duplicates rather than failing start-up. Soft delete releases keys in the same transaction, so a released key is immediately reusable.
Breaking: Props-level lazy loading is replaced by lazy references
EnableLazyLoadingForProps,WithLazyLoading()and thelazyLoadPropsparameter of everyLoadAsyncoverload are gone. The mechanism made the cheap part lazy (one object's scalars) and kept the expensive part eager (the reference graph to depth 10), sat behind three switches and had no tests.What replaces it works the other way round.
LoadAsync(depth: 1)returns a reference at the depth boundary as a stub that carries a loader: touchingPropsloads exactly that object, whose own references are stubs again. Laziness is transitive and independent of the original depth.LoadReferencesAsync(parent, p => p.Children)reloads a collection of stubs in one batch, and a reference property declaredvirtualis marked in the scheme, so the JSON builders emit a stub for it regardless of depth.Migration is mechanical: delete the flag, the call and the parameter. The default was off, so an unconfigured project behaves identically, with one exception that needs no flag. A reference beyond
depthused to be a stub whosePropswere simply null; it now carries a loader, and readingPropsis a synchronous round trip. The load runs on the thread pool, so a host with aSynchronizationContext(Blazor Server, WPF, WinForms, MAUI) waits instead of deadlocking. Blazor WebAssembly cannot block at all: setLazyReferenceAccess = Throwthere and reload explicitly withLoadPropsAsyncorLoadReferencesAsync. The same switch suits any UI host where a hidden query on property access is a defect.Security: bcrypt is the default password hasher
From an external report. Every provider's stock user-provider factory hard-wired SHA256 with a salt, and
IPasswordHasherwas not registered in DI at all, soBcryptPasswordHashersat in the tree unreachable.AddRedbnow registers bcrypt throughTryAdd, so your own registration still wins, and the factories resolve it from DI. Existing databases are safe: verification recognises both formats, legacy hashes keep authenticating and migrate to bcrypt on the next password change.The API grew the things applications kept writing by hand
CancellationTokenacross the entire async API (discussion Some comments after some usage of redb #12). Every async method takes one; existing calls compile unchanged.IRedbSaveInterceptor, write-lifecycle interceptors shaped like EF's (discussion Some comments after some usage of redb #12).IRedbService.Maintenance, a storage maintenance facade over the provider-specific statistics and index work.BeginTransactionAsync(IsolationLevel?)andExecuteAtomicAsync(IsolationLevel, ...). Nothing changes without the parameter. Under elevated levels the database may abort the loser, andDbErrorClassifier.IsSerializationFailure(ex)classifies that without provider-specific code.byte[]is stored as the scalar BLOB it is. The scheme sync used to classify it as an array of Byte, so a megabyte of payload became a million rows. It is one_values._ByteArrayvalue now, and a database with the old layout heals itself on the next synchronisation.Orderclasses in two projects no longer quietly share one scheme: the mismatch is a typed exception naming both owners and the way out.Fixed, mostly from production stands
The connection-level work is the part worth reading in full in the changelog: a sync getter that could seize the process, a lazy reference loading through a dead scope, scope teardown racing an in-flight command, an id-generator self-deadlock on SQLite inside the caller's transaction, a failed save poisoning the next saves of its scope, and cross-database poisoning of the structure-tree cache when two services on different databases shared a process.
NpgsqlDataSourceandSqliteDataSourceare owned by the container now.Pro on PostgreSQL and SQLite never upgraded an existing database at all: a 4.x start over a 3.x database died in the provider's own ensure path. The upgrade contract is checked on all six Free and Pro fixtures now.
Upgrading
Read this part if
_valuesis large.The V4 schema block adds columns and builds two partial unique indexes inside the module bundle, which runs as one command at start-up. On PostgreSQL the
ALTER TABLE ... ADD COLUMNstatements holdACCESS EXCLUSIVEuntil the bundle ends, and the index build scans_values; on MSSQL the offlineCREATE INDEXholds a table lock for its duration.CONCURRENTLYandONLINE = ONcannot run inside an atomic bundle. On a multi-gigabyte_values, treat the first start of 4.0 as a maintenance step: applyGetUpgradeScript()orredb schema --upgradefrom one node in a quiet window, and start the application nodes withAutoApplyDatabaseUpgrades = falseso none of them races the DBA. A pre-V4 database with a small_valuesneeds nothing special.Two more upgrade notes.
_objects._value_stringis an identifier column now, 450 characters, indexed on MSSQL too: a database holding longer values refuses the upgrade loudly and names what is in the way. The MSSQL full-text index on_values._Stringis dropped, because redb never generatesCONTAINS()orFREETEXT()and the index paid a background reindex on every write.A database whose role was stripped of DDL rights after installation no longer gets a raw driver error from the middle of a bundle. It gets
RedbSchemaOutdatedExceptionnaming the deployed and the required version, with the privilege error as its cause.If you are on 3.7.0
3.7.0 was withdrawn the day after release: it was built on .NET 9 with vulnerable dependencies, and every 3.7.0 package is unlisted on nuget.org. 3.7.1 and 3.7.2 replaced it in August and went out without an announcement here. 4.0.0 supersedes all three.
Links
Questions, or an upgrade that does not go the way this describes? Reply here or open a thread in Q&A. Anyone can post, no permissions needed.
All reactions