v3.4.0
Why 3.4.0 (a minor bump), not 3.3.4. This release adds features, not just fixes: explicit scheme
names ([RedbScheme(Name = "...")]), scheme-name validation triggers on MSSql/SQLite, and the new
ThrowOnSchemeMismatchload-time guard. Under SemVer that is a minor version. The core packages
(RedBase.Core, the three providers Free/Pro,RedBase.Export,RedBase.CLI) move together to
3.4.0;redb.Route,redb.Tsakandredb.Identitykeep their own version lines.
Added
-
Explicit scheme names —
[RedbScheme(Name = "...")](RedBase.Core+ all providers). Until now
a scheme was always named after the CLR type'sFullName, and the string in the attribute was only
a cosmetic_alias. A scheme name can now be pinned explicitly, which decouples the database
identity of a scheme from C# namespace/class refactoring.The positional argument is, and stays, the alias — an explicit name is only settable through the
namedNameparameter, so not one existing declaration changes meaning. Types that declare no
Namebehave exactly as before.On the first sync a type with an explicit name has its scheme renamed in the database. The
scheme is looked up along a three-step chain — explicit name,FullName, short type name — and the
first match is renamed in place. The rename is a single-rowUPDATEof_schemes._name: the id is
preserved, so objects, structures and values are untouched, and polymorphic loading (which resolves
throughscheme_id) is unaffected.Renaming requires every consumer of that database to be updated together. An application
version that predates the explicit name will not find the scheme under its new name and will
create a second one, splitting objects between them silently. If that has already happened, redb
now detects it and refuses to continue rather than picking one of the two.Explicit names must follow C# identifier rules (Latin letters, digits,
_,.,+; no reserved
words; 128 characters max) and are validated in C# before any SQL is issued, so the error names the
offending type. Human-readable titles belong inAlias, which is free-form. -
Scheme-name validation in MSSql and SQLite (
RedBase.MSSql,RedBase.SQLite). Both providers
now carry a_schemesname-validation trigger mirroring the PostgreSQLvalidate_scheme_name()
rule for rule, so a name accepted by one provider is accepted by all three. Applies to
newly created databases only —EnsureDatabaseAsyncskips initialisation when_schemes
already exists. The C#-level validation covers databases of any age. -
RedbServiceConfiguration.ThrowOnSchemeMismatch(RedBase.Core, defaultfalse). Chooses how
LoadAsync<TProps>reacts when the object's scheme does not matchTProps(see Fixed):false
returnsnull(so a soft-deleted object, scheme-10, reads asnull— what soft-delete callers
expect);truethrowsRedbSchemeMismatchExceptionto surface a genuine type mistake loudly.
Changed
- The SQLite native extension is renamed
redb→redbsqlite(RedBase.SQLite, Free tier).
The package now shipsruntimes/<rid>/native/redbsqlite.{dll,so}(win-x64, linux-x64, linux-arm64)
instead ofredb.{dll,so}. The generic name collided with the managedredb.*assemblies and with
theredb.*prune globs a host applies to its own bin directory — a native loadable module was
being swept up as if it were one of ours. The C init symbol is unchanged (sqlite3_redb_init)
and the binaries are byte-for-byte the ones shipped asredb.*in 3.3.3: this is a rename of the
file, not a rebuild, andLoadExtensionhas always been called with an explicit entry point.- Nothing to do if you let the package resolve the extension (
SqliteDataSource .LocatePackagedExtension(), the default for the Free DI registration) — it looks for the new name. - Action required if you pin the path yourself —
REDB_SQLITE_EXTENSION, an explicit
NativeExtensionPath, a DockerfileCOPY, or a deploy script that copiesredb.soby name:
point it atredbsqlite.{dll,so}. A stale path fails at connection open, not at build.
- Nothing to do if you let the package resolve the extension (
Fixed
-
Concurrent start-up of several nodes could fail to create a scheme (
RedBase.Core+ all
providers). Reproduced in production on a three-node cluster. When several instances started
against a database that did not yet have a given scheme, all of them missed the lookup and all
issuedINSERT INTO _schemes; every instance but one failed with an unhandledUNIQUE(_name)
violation during initialisation.Scheme creation now uses a conflict-free statement per dialect (
ON CONFLICT (_name) DO NOTHINGon
PostgreSQL and SQLite,INSERT ... WHERE NOT EXISTSwithUPDLOCK, HOLDLOCKon MSSql) and the
instance that loses the race reads back the winner's row. Catching the violation instead would not
have worked: on PostgreSQL a failed statement inside a transaction poisons it, so the follow-up
read would fail with25P02. Both creation paths are covered — typed
(EnsureSchemeFromTypeAsync<T>) and untyped (EnsureObjectSchemeAsync). -
Pro data migrations never ran — on any provider (
RedBase.Core.Pro,RedBase.SQLite,
RedBase.MSSql). Four separate defects stacked on top of each other, and the feature had no test
coverage at all, so none of them had ever surfaced:MigrationExtensions.CreateExecutorfetched the DB context by reflection asking for a
NonPublicContextproperty, butRedbServiceBase.Contextis public — the lookup never
matched and every migration died with "Cannot get IRedbContext from RedbService". It now uses
redb.Contextand(ISchemeSyncProvider)redbdirectly; both are part ofIRedbService.- The generated UPDATE was hardcoded to PostgreSQL syntax (
UPDATE _values target ...). SQLite
forbids aliasing an UPDATE target (near "target": syntax error) and T-SQL needs the alias
bound in a trailingFROM. AddedISqlDialectPro.Migration_UpdateTarget(table, alias)with
one implementation per provider. - SQLite had no
_migrationshistory table at all: PG/MSSql get it from the concatenated
redb_init.sql, and SQLite has no such concatenation step, so the sharedredb_migrations.sql
never reached it. A SQLite-native DDL (INTEGER PK, REAL Julian_applied_at, INTEGER
_dry_run) now ships inredbSqlite.sql. - MSSql declared
_migrations._idasIDENTITY(1,1)— the only IDENTITY in the whole MSSql
schema — while the executor supplies ids explicitly like every other redb table, so writing
history failed with "Cannot insert explicit value for identity column". IDENTITY removed.
Covered by a new
MigrationTestsBasesuite (apply / history row / idempotency / dry-run) running
against all three Pro fixtures. Note: existing databases do not receive the corrected_migrations
DDL, sinceEnsureDatabaseAsyncskips initialisation when_schemesalready exists — but no
database can hold real migration history, because the feature could not complete a single run. -
A scheme's
_aliaswas never updated after creation (RedBase.Core+ all providers). It was
written once onINSERTand then frozen: changing[RedbScheme("...")]on a class left the old
value in the database forever. Structure aliases have always synchronised (Structures_UpdateAlias);
schemes simply had no equivalent. They do now — the attribute is the source of truth, removing it
resets_aliastoNULL, and a value edited by hand in the database is overwritten on the next sync. -
ClrSchemeTypeIndexpinned hot-reloaded plugin modules in memory (RedBase.Core). The
process-globalschemeName → Typeindex held strongTypereferences, and aTypekeeps its
AssemblyLoadContextalive — so a collectible ALC (Tsak hot-swap) could never be collected, and after
a reload the stale instance produced a falseRedbSchemeNameConflictExceptionagainst the fresh one,
blocking the module from loading. Entries are nowWeakReference<Type>(dead ones pruned on lookup),
and two instances of the sameFullNamefrom different ALCs are recognised as a reload, not a name
clash. Distinct types sharing one explicitNamestill conflict, by design. -
LoadAsync<TProps>did not verify the loaded object's scheme (RedBase.Core+ all providers).
Loading an object of one scheme under an unrelatedTPropsdeserialised garbage into the Props and —
withEnablePropsCache— cached it underobjectId, so a laterGetWithoutHashValidationkept
returning the garbage.LoadAsync<TProps>now checks the object's_id_schemeagainst the scheme
TPropsmaps to before anything is cached. By default a mismatch returnsnull— so a soft-deleted
object (scheme-10, set bySoftDeleteAsync) reads asnull, which is what soft-delete callers
expect; setRedbServiceConfiguration.ThrowOnSchemeMismatch = trueto instead throw
RedbSchemeMismatchExceptionon a genuine type mistake. Either way garbage never reaches the cache.
The untypedLoadAsync(objectId)is never affected. -
Invalid explicit scheme names failed one at a time (
RedBase.Core). Auto-sync validates every
[RedbScheme(Name = "...")]and (by design) refuses to boot on an invalid name — but it stopped at
the first offender, so a codebase with several had to be fixed one rerun at a time. Names are now all
validated up front and reported together in a singleAggregateException.
Removed
- Dead metadata-cache interface layer (
RedBase.Core).ICompositeMetadataCache,
ISchemeMetadataCache,IStructureMetadataCache,ITypeMetadataCache,IStaticMetadataCacheand
StaticMetadataCache— 679 lines across five files that referenced only each other. None was ever
implemented, none was ever consumed; the live caches areGlobalMetadataCache(per cache domain),
GlobalListCache,GlobalPropsCacheandClrSchemeTypeIndex(per process). Formally a breaking
change since the types were public, but they could not be used for anything: the interfaces had no
implementations.CacheDiagnosticInfo,CacheHealthStatus,MemoryUsageInfoandPerformanceInfo
lived in the same file and are part of the liveISchemeCacheProvidercontract — they moved
unchanged toredb.Core/Caching/CacheDiagnosticInfo.cs.redb.Core/Caching/README.md, which
documented only the removed layer, has been rewritten to describe the caches that actually exist.