Skip to content

Extracted translations - #99

Open
eMukator wants to merge 109 commits into
NTDLS:mainfrom
eMukator:main
Open

Extracted translations#99
eMukator wants to merge 109 commits into
NTDLS:mainfrom
eMukator:main

Conversation

@eMukator

Copy link
Copy Markdown
Collaborator

Hi @NTDLS, I made a few small adjustments that I hope will speed up the build.

Milan Chvojka and others added 30 commits August 16, 2026 11:00
Retype DatabaseManager's repository properties to concrete implementations (with explicit
ITwDatabaseManager interface members) instead of exposing the SQLite-specific
SqliteManagedFactory through the plugin contracts, so ITwDatabaseManager and the
7 repository interfaces stay provider-agnostic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add GetDefaultConfigurationGroups/GetDefaultConfigurations/GetDefaultThemes/GetDefaultWikiPages/
GetDefaultFeatureTemplates to the ITwDefaultsRepository contract, implemented in DefaultsRepository,
and switch DatabaseManager.ApplyAllSeedData to call them instead of reaching into DefaultsFactory directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Register DatabaseManager as ISpannedRepository in DI and route AdminController's database admin
actions through it instead of the concrete DatabaseManager. Also fixes a bug where the Vacuum and
Verify actions both mistakenly called OptimizeDatabase instead of VacuumDatabase/IntegrityCheckDatabase.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ap to interface type

Expose schema initialization through the provider-agnostic ITwDatabaseManager contract instead of
calling the SQLite-specific ApplyDatabaseUpgradeScripts directly, and type Program.cs's bootstrap
variable as ITwDatabaseManager. Also derive the Identity connection string straight from
configuration (GetIdentityConnectionString) rather than reading it off a live UsersRepository
instance, mirrored in MockWikiEngineArtifacts for the test host.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Relocate WikiConfigurationManager out of TightWiki.Repository.Helpers into TightWiki.Library and have
it depend on ITwDatabaseManager instead of the concrete DatabaseManager, removing the last place
outside TightWiki.Repository that referenced the SQLite-specific implementation directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Relocate GuidTypeHandler from TightWiki.Library into TightWiki.Repository.Helpers, since it is a
Dapper/SQLite-specific concern rather than cross-cutting infrastructure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Introduce a DataProvider MSBuild property (defaulting to Sqlite) that drives a SQLITE_PROVIDER
define constant plus conditional PackageReference/ProjectReference entries, and wrap the
SQLite-specific bootstrap code in Program.cs behind #if SQLITE_PROVIDER so a future DataProvider
value can supply its own implementation. Also de-couples the GetIdentityConnectionString doc
comment from a conditional type reference.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Registers the new provider-agnostic EF Core project in TightWiki.sln
and excludes its bin/obj output via .gitignore.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds standalone SchemaUpgradeTool (brings a working copy of the 8 SQLite
db files to latest schema) and EfScaffoldTool (hosts dotnet-ef design-time
dependencies without pulling SQLite into the provider-agnostic
TightWiki.Data.EfCore project), orchestrated by Generate-EfMigrations.ps1.

Checks in the tool's output under TightWiki.Data.EfCore/_Scaffold/ as
reference material for the next task's manual DbContext/entity merge
(Database-Providers-Plan.md ch. 5); excluded from compilation via
<Compile Remove> in the csproj.
Model the Config, Statistics, Logging, and Emoji database schemas as
EF Core entities and Fluent API configurations under
TightWiki.Data.EfCore, and reference Microsoft.EntityFrameworkCore.Relational
for the schema/collation/default-value APIs the configurations use.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…evisions

Continues the fluent-API entity/configuration pass from d30622c, covering
the three remaining SQLite-backed databases (Pages, DeletedPages,
DeletedPageRevisions) with their entities and matching IEntityTypeConfiguration
classes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers Role, Permission, PermissionDisposition, Profile, AccountRole,
AccountPermission, RolePermission, and AdminPwCheck, mirroring the
Users.db table set with matching fluent configurations.
Adds DbSet<T> properties for all 45 entities across the 8 logical
schemas, switches OnModelCreating to ApplyConfigurationsFromAssembly,
and changes the constructor to accept the non-generic DbContextOptions
so per-provider driver projects can configure the shared context.

Also adds the cross-schema navigations (Pages/DeletedPages/
DeletedPageRevisions/Statistics -> Users.Profile) and their reverse
collections on Profile, needed to model the 8 SQLite databases as one
logical database with 8 schemas per Database-Providers-Plan.md 4.3.
GenerateSeedData now also builds Seed\tightwiki.seed.zip, a provider-neutral
seed package (per Database-Providers-Plan.md ch. 4.6b) read from the live
Data\*.db SQLite files, meant for TightWiki.Data.EfCore to seed a fresh
MSSQL/Postgres database. It contains one JSON manifest per table
(ConfigurationGroup/Entry, MenuItem, Theme, FeatureTemplate, DefaultWikiPages
per namespace, Emoji/EmojiCategory) plus emoji images stored as separate zip
entries rather than base64.

The DataType lookup table is intentionally not included - per the plan it
belongs in the HasData migration added in phase 2a, not this export.
SQLite provider returns empty collections since it seeds Emoji via a full
copy of Data\emoji.db rather than through this mechanism; the contract
exists for future EF-based providers seeding from tightwiki.seed.zip.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e 1b.3)

Implements ITwDefaultsRepository over Seed\tightwiki.seed.zip, letting
MSSQL/Postgres driver projects seed a freshly created database without
touching SQLite at runtime.

Known gap: TightWiki.Plugin still carries a NTDLS.SqliteDapperWrapper
PackageReference (SqliteManagedInstance used in 3 ITwPageRepository.cs
method signatures, pre-existing debt from 754cc5e), so the new
ProjectReference on TightWiki.Plugin transitively pulls in
Microsoft.Data.Sqlite.Core/Dapper/SQLitePCLRaw.* packages even though
none of that code is exercised here. Documented in the csproj rather
than worked around, since fixing it is out of scope (chapter 6, phase 2b).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The interface exposed 3 method overloads taking a raw SqliteManagedInstance
connection parameter, which meant the plugin contract carried a direct
NTDLS.SqliteDapperWrapper package reference just to compile - even though
TightWiki.Plugin is meant to be database-agnostic (and is published as a
standalone NuGet package for third-party plugin authors). Dropping those
overloads lets the SQLite dependency move down to where it's actually used:
TightWiki.Repository (the Dapper/SQLite implementation) and LocalizerScan
(which touches the repository directly for scanning).

This finishes the "Bonus" cleanup flagged in phase 0 chapter 4.1 of the
Database-Providers-Plan: the plugin contract no longer pulls in SQLite
packages, so TightWiki.Data.EfCore's ProjectReference on TightWiki.Plugin
is now transitively clean, per the "no SQLite package in
TightWiki.Data.EfCore" rule in chapter 9.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Emoji.ImageData in the source emoji.db is stored GZip-compressed
(matching the runtime FileController's Compress/Decompress round trip),
but SeedPackageGenerator wrote the raw column bytes straight into the
zip's Emoji/Images/* entries. Every .png/.gif/.jpg entry therefore
contained a gzip stream instead of an actual image, despite the
correct extension.

Decompress ImageData when its bytes carry the GZip magic number (1F 8B)
before writing the zip entry, and derive the file extension from the
decompressed content's real byte signature (PNG/GIF/JPEG/WEBP/BMP)
instead of trusting the MimeType column, falling back to the MIME-based
mapping only for signature-less formats like SVG.

Regenerated Seed/tightwiki.seed.zip with the fix; independently
verified by the tester at 100% coverage across all 1949 emoji images.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…se 2a.1)

Introduces a thin MSSQL/EF Core driver project (SqlServerDatabaseManager plus
per-table repository stubs) and wires it into the conditional DataProvider
build: TightWiki.csproj now defines SQLSERVER_PROVIDER and references the new
project under -p:DataProvider=SqlServer, Program.cs selects
SqlServerDatabaseManager under that constant, and TightWiki.sln/.gitignore
are updated accordingly.
Applies ApplicationDbContext (ASP.NET Core Identity) under the "Users"
schema alongside TightWikiDbContext, both migrated at startup via
SqlServerDatabaseManager.InitializeSchema (Identity first). Adds
separate MigrationsHistoryTable names/schemas per context
(SqlServerMigrationsHistory) so the two DbContexts sharing one
database don't collide on EF Core's default __EFMigrationsHistory
table, wires up ConnectionStrings:TightWikiEfCore for
ApplicationDbContext under SQLSERVER_PROVIDER in Program.cs, and
strips the SQLite-only NOCASE collation from non-SQLite providers in
TightWikiDbContext.OnModelCreating.
Add HasData for Severity, PermissionDisposition, Permission, Role, and
DataType in TightWiki.Data.EfCore configurations, and generate the
matching SeedStaticLookups migration for TightWiki.Data.EfCore.SqlServer.
…er (phase 2a.4)

Vacuum uses per-table ALTER INDEX REBUILD, Optimize runs sp_updatestats, and
IntegrityCheck/ForeignKeyCheck run DBCC CHECKDB/CHECKCONSTRAINTS. Versions and
page counts/sizes report per-schema across the 8 consolidated schemas, sourced
from EF Core migrations history and sys.dm_db_partition_stats respectively,
replacing the NotImplementedException stubs. Also renames the versions getter
to GetDatabaseVersions to match the other ISpannedRepository members.
Add SqlServerDatabaseManager.ApplyAllSeedData() to import configuration,
themes, feature templates, wiki pages, emoji + categories, and menu items
from Seed/tightwiki.seed.zip, GZip-compressing emoji images and bootstrapping
the admin user. Add GetDefaultMenuItems() to ITwDefaultsRepository,
EfDefaultsRepository, and DefaultsRepository to support the import.
Add provider-agnostic ITwConfigurationRepository implementation over LINQ
in the shared TightWiki.Data.EfCore project, wire it into
SqlServerDatabaseManager in place of the NotImplementedException stub,
and remove the now-obsolete SqlServerConfigurationRepository stub.
Add EfLoggingRepository as the provider-agnostic ITwLoggingRepository
implementation, wire it into SqlServerDatabaseManager in place of the
SqlServerLoggingRepository stub, and promote Logger from a plain
console logger to DatabaseLogger once LoggingRepository is available -
mirroring the SQLite DatabaseManager's two-stage bootstrap.
Replaces the SqlServerEmojiRepository stub with a provider-agnostic
LINQ-over-EF-Core implementation of ITwEmojiRepository, mirroring the
SQLite reference (EmojiRepository + Scripts/*Emoji*.sql) method by
method, including its GZip image compression and case-insensitive
category handling. Deliberately does not reproduce the SQLite
reference's UpsertEmojiCategories.sql DELETE bug (hardcoded
EmojiId = 1 instead of @EmojiId) and instead implements the evidently
intended per-emoji category pruning semantics.
Adds the provider-agnostic LINQ-over-EF-Core implementation of
ITwStatisticsRepository, fixing the SQLite reference's missing
Namespace population in GetPageStatisticsPaged along the way. Wires
SqlServerDatabaseManager.StatisticsRepository to the new type and
removes the now-superseded SqlServerStatisticsRepository stub.
…(phase 2a.10)

WikiConfigurationManager is constructed before builder.Build() and eagerly
reads Config.Theme, which crashes the app on a freshly migrated but
unseeded MSSQL database. Split SqlServerDatabaseManager.ApplyAllSeedData
into a new DI-free SeedContentDataAsync (everything except EnsureAdminUser,
which needs a UserManager<IdentityUser>) and call it from Program.cs right
after InitializeSchema, before the DI container is built. ApplyAllSeedData
is now a thin wrapper that ensures the admin user then delegates to
SeedContentDataAsync, still called post-Build as before.
Relocates the SQL Server driver's PageRepository and UsersRepository
skeletons (still NotImplementedException stubs) from
TightWiki.Data.EfCore.SqlServer to the provider-agnostic
TightWiki.Data.EfCore project as EfPageRepository/EfUsersRepository,
matching the pattern already used for Configuration/Logging/Emoji/
Statistics. Real LINQ-based implementations land across phases 2b.2-2b.13.
Milan Chvojka and others added 29 commits August 24, 2026 14:19
Fifth repository test class in the series, covering the reader for the
tightwiki.seed.zip package (emoji, config, users, groups, permissions,
pages+revisions, attachments, search metadata). Verifies the intended
asymmetry: SqliteDefaultsRepository hardcodes empty results for 4 of the
9 methods (revisions, attachments, permissions, search metadata), while
the EF providers (SqlServer/Postgres) read real seed data from the zip.
First of three sub-tasks covering ITwUsersRepository (51 members total):
this one covers the role slice (13 members) - GetAllRoles, GetRoleByName,
DoesRoleExist, AutoCompleteRole, InsertRole, DeleteRole,
AddRoleMember(Byname), AddAccountMembership, RemoveRoleMember,
IsAccountAMemberOfRole, GetRoleMembersPaged and
GetAccountRoleMembershipPaged.

Mutating tests grant/revoke roles on the seeded admin account rather than
creating new test profiles, since ITwUsersRepository has no way to delete
a profile - a leftover test profile would permanently contaminate the
shared test database and break the golden-file ProfileList/ProfileGlossary
expectations.

Verified independently against SQLite, SqlServer and Postgres providers.
Scenario-level integration tests for the permissions slice of
ITwUsersRepository (AccountPermission/RolePermission CRUD, cross-schema
Users<->Pages ResourceName resolution) and the 4-member admin-default-
password state machine (AdminPasswordStatus/SetAdminPasswordClear/
SetAdminPasswordIsDefault/SetAdminPasswordIsChanged).

The AdminPasswordStatus state-machine round-trip test wraps its final
SetAdminPasswordClear() cleanup call (which resets the shared test
database to "NeedsToBeSet" for SqlServer/Postgres re-runnability) in a
finally block, matching the try/finally cleanup pattern used elsewhere
in this class for role/permission cleanup - so a transient failure on
that specific call can't skip the restore and leave the process-wide
AdminPasswordStatus cache poisoned for the next test run.

Verified 7/7 passing against SQLite (default), SqlServer, and Postgres.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers the 17-member user-profile category of ITwUsersRepository -
AutoCompleteAccount, GetAll(Public)Users(Paged), GetAccountProfileBy*,
GetBasicProfileByUserId, GetUserAccountIdByNavigation,
DoesProfileAccountExist/DoesEmailAddressExist, IsUserMemberOfAdministrators,
and UpdateProfile/UpdateProfileAvatar round-trips against the seeded admin
account. Completes ITwUsersRepository coverage (51 methods total, all three
sub-tasks now done).

CreateProfile/AnonymizeProfile/SetProfileUserId are reviewed by code
inspection only, not invoked live: the interface has no member to delete a
Users.Profile row, so a CreateProfile-created row would permanently pollute
the golden ProfileList/ProfileGlossary .wiki.expected files, and
Anonymize/SetProfileUserId both mutate the identity fields
(AccountName/Navigation/UserId) that the sibling role/permission test
classes rely on to resolve the same shared seeded admin row concurrently.
…ents/metadata

First of four ITwPageRepository test tasks (86 members total), covering the 30
members handling autocomplete, page-cache flushing, page comments,
current-page-editors, and page/revision metadata reads. Written against the
provider-agnostic ITwPageRepository interface, so the same compiled test code
runs against SQLite (default), SqlServer, and Postgres via -p:DataProvider=.

GetTopRecentlyModifiedPagesInfo's ordering is verified indirectly: the method
orders by Page.ModifiedDate but returns the joined current PageRevision's
ModifiedDate, so each returned row's real Page.ModifiedDate is independently
re-fetched via GetPageInfoByNavigation and descending order checked against
those values instead - GetTopRecentlyCreatedPagesInfo/GetTopViewedPagesInfo/
GetTopEditedPagesInfo verify ordering directly since their returned column
matches their ORDER BY source.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers the 10 bulk/paged-listing ITwPageRepository members (GetAllPagesPaged,
GetAllNamespacePagesPaged, GetAllDeletedPagesPaged, GetMissingPagesPaged,
GetAllPagesByInstructionPaged, GetAllNamespaces(Paged), GetAllPages,
GetAllTemplatePages, GetAllFeatureTemplates), including the TempPageIds/
Contains(...) search-term substitution pattern and pagination boundaries.

Ordering assertions are strengthened over the initial draft: DeletedRevisions
is a numeric column (TwPage.DeletedRevisionCount) despite its string orderBy
key, so it now gets the same strict monotonic check as Revision/ModifiedDate
instead of a bare non-empty check. Name ordering is verified collation-
independently by concatenating every page of both asc and desc results and
asserting the full sequences are exact reversals of each other (Name is
unique across all 110 seeded pages) - single-page comparison was unsound
since 110 rows / a pagination size of 20 doesn't divide evenly, so page 1
asc/desc cover disjoint, non-reversed slices. ModifiedBy is left as a
does-not-throw check only, since every seeded page shares the same modifying
account (Admin) and a reversal check there would be vacuous.

Verified 6/6 against SQLite (default), SqlServer, and Postgres providers.
Adds PageRepositorySearchTests.cs covering the third of four
ITwPageRepository test tasks: the 15 fuzzy search, tag, and token
members (PageSearch/PageSearchPaged, GetSimilarPagesPaged,
GetRelatedPagesPaged, GetBacklinkPagesPaged, token/tag CRUD, and
ParsePageTokens). Assertions are deliberately weak (Assert.Contains,
relative orderings) where Double Metaphone fuzzy matching and the
absence of ORDER BY make exact results non-deterministic across
providers. Verified 3x against SqlServer and Postgres plus 2x full
SQLite suite runs with no regressions by an independent tester.
Fourth of five ITwPageRepository test tasks (86 members split across
files, same pattern as PageRepositoryMetadataTests/ListingTests/
SearchTests). Covers UpsertPage (via RefreshPageMetadata/
UpdatePageProcessingInstructions/UpdateSinglePageReference/
UpdatePageReferences) and UpsertPageFile plus the 11 page file/
attachment members (detach, orphan listing/purge, revision reads).

Adds PageRepositoryCrudAttachmentTests.cs (7 tests):
- UpsertPage inserts a new page at revision 1, populates processing
  instructions and outgoing references via RefreshPageMetadata
- UpsertPage hash-based change detection: only bumps the revision when
  content actually differs (CRC32 mismatch); re-saving identical
  content is a genuine no-op
- UpsertPage resolves pre-existing orphan references via
  UpdateSinglePageReference when the target page is later created
- UpsertPageFile inserts a new attachment at revision 1
- UpsertPageFile hash-based dedup: re-uploading identical bytes does
  not bump FileRevision
- UpsertPageFile bumps FileRevision on different content and preserves
  the previous revision's content
- DetachPageRevisionAttachment orphans a file revision, which
  GetOrphanedPageAttachmentsPaged surfaces and
  PurgeOrphanedPageAttachment removes permanently

Written entirely against the provider-agnostic ITwPageRepository
interface, no #if provider branching - runs unchanged against SQLite
(default), SqlServer, and Postgres.

Also documents (in the class remarks, not fixed here) an independently
confirmed pre-existing bug: FlushPageCache, called at the end of both
MovePageToDeletedById and PurgeDeletedPageByPageId, re-queries
Pages.Page for the page's navigation by pageId *after* the row has
already been deleted, so that lookup returns null and the
navigation-keyed GetPageInfoByNavigation cache entry for a just-deleted
page is never reliably invalidated this way (only pageId-keyed entries
are). This is replicated from the SQLite reference implementation, not
introduced by this work - flagged here as a candidate for a future,
separate bugfix task. This test class works around it by never calling
GetPageInfoByNavigation against any page it creates and deletes itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ges<->DeletedPageRevisions)

Covers page and revision deletion/restore, including the transactional
move of rows between the Pages, DeletedPages, and DeletedPageRevisions
tables. This is the 5th and final of 5 sub-tasks covering
ITwPageRepository (86 methods) — the interface is now fully covered by
tests. Verified 3x in a row against SqlServer and Postgres providers,
plus 3x against the full SQLite suite with no regressions.
… fixes

Locks down 4 bugs found and fixed during the MSSQL/PostgreSQL provider
implementation so they cannot silently regress:
- missing RolePermission HasData seed (b354acf)
- missing AccountRole after SQL Server admin bootstrap (48039b8)
- FK conflict in admin bootstrap, code-review-only case (069507f)
- IDENTITY_INSERT finally block not always executed (0ac968b)
Covers Postgres raw-SQL insert column quoting (3d8512d), NOCASE/citext
collation for case-insensitive lookups (8c02b34), root/Sandbox namespace
seeding plus page attachments (a71ee1a), and emoji GZip corruption in the
seed package export (7eb2c32), so these already-fixed bugs cannot
silently regress on the MSSQL/Postgres providers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tions

Covers the 7 admin/DBA-maintenance members exposed on ITwDatabaseManager
(VacuumDatabase, OptimizeDatabase, IntegrityCheckDatabase, ForeignKeyCheck,
GetDatabaseVersions, GetDatabasePageCounts, GetDatabasePageSizes) behind the
AdminController "Database" screen. Written against the provider-agnostic
interface only, so the same compiled test runs unmodified against SQLite,
SqlServer, and Postgres. Closes out the T4 test coverage plan.
T5, the last major test block from the testing plan: exercises the full
end-to-end schema-init + seed bootstrap (InitializeSchema + ApplyAllSeedData)
against a genuinely fresh, dedicated database per provider (TightWikiFreshTest
/ tightwiki_fresh_test for MSSQL/Postgres, a scratch temp-directory copy of
the shipped Data\*.db files for SQLite) rather than the shared TightWikiTest
database every other test file relies on. Also explicitly proves that a
second InitializeSchema/ApplyAllSeedData run against an already-initialized
database is a no-op (no re-reported upgrade, no duplicated config/page rows),
and cleans up its dedicated database before and after the run.
Adds Verify-PublishOutput.ps1, which does a Release publish with
-p:DataProvider=SqlServer/Postgres and fails if the output contains any
SQLite/Dapper DLL (TightWiki.csproj only pulls those in for
DataProvider=Sqlite). Wires it into both CI matrix legs (SqlServer,
Postgres) as the final Database-Providers-Testing-Plan.md task (T6),
replacing what was previously a manual per-phase tester check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
xunit v2 by default runs different [Collection]s concurrently with each
other, even though tests within one collection are already serialized.
All test classes here share the same persistent seeded database, and
several mutate it (PageRepositoryDeleteRestoreTests etc.) while others
read unfiltered/whole-table listings (FullPageTests, PageRepositoryListingTests
via RecentlyCreated/RecentlyModified/MostEdited/GetAllPagesPaged) with no
scoping to their own rows - those reads could observe transient in-flight
state from a concurrently-running mutating collection and fail
non-deterministically. Reproduced case:
PageRepositoryListingTests.GetAllPagesPaged_DefaultAndExplicitOrdering_ReturnsConsistentPagination
(finding NTDLS#5 in the DB-providers testing initiative).

CollectionDefinitions.cs centralizes all 19 [CollectionDefinition]
declarations with DisableParallelization = true, serializing collections
against each other while leaving intra-collection test execution
unchanged. Verified by an independent tester: identical failure sets
across repeated runs on all 3 providers.
…ovePageToDeletedById

FlushPageCache re-queried Pages.Page for the page's own navigation after the
row was already deleted, so the query always returned null and the primed
GetPageInfoByNavigation cache entry for that navigation was never actually
cleared - the app could keep serving a deleted page's cached content, or
reject re-creating a page under the same navigation, until TTL expiry.

MovePageToDeletedById (both PageRepository and EfPageRepository) now resolves
the navigation before deleting the row and passes it into FlushPageCache's
new optional navigation parameter, which is used as-is instead of being
re-queried. Backward compatible: existing callers passing only pageId keep
the old lookup-by-id behavior unchanged.

Adds a regression test that fails against the pre-fix implementation (stale
cached page survives the delete) and passes against the fix.
DatabaseManager.IntegrityCheckDatabase concatenated ForeignKeyCheck(databaseName)
without awaiting it, appending Task<string>'s object.ToString() ("System.Threading.
Tasks.Task`1[System.String]") after "ok" instead of the actual foreign-key check
result. The SQLite admin "Verify" screen showed this garbled text. Fixed by
awaiting the call, so a healthy database now reports a clean "ok".

Strengthened SpannedRepositoryAdminOperationsTests' SQLite assertion from a
StartsWith("ok") prefix check (previously tolerant of the bug's trailing
artifact) to an exact Equals("ok") match, and updated the doc comment
accordingly.
…roException)

EfPageRepository computed the pagination page count via C# integer division
((totalCount + (pageSize - 1)) / pageSize) with no guard against pageSize=0,
so any call with an explicit pageSize of 0 threw DivideByZeroException on
SqlServer/Postgres. This is a real, provider-independent production bug, not
a test artifact: wiki markup functions like ##Revisions, ##SearchList,
##Attachments, and ##Related can be invoked with an explicit pageSize of 0.
The SQLite/Dapper reference implementation never hit this because SQL's
LIMIT 0 simply returns zero rows before any division ever happens.

Adds a `pageSize == 0 ? 0 : ...` guard at all 16 call sites in the file that
follow this pattern, replicating SQLite's LIMIT-0 semantics (zero pages) in
C#. Also adds PaginationEdgeCaseRegressionTests.cs, which reproduces the
DivideByZeroException against the old code and passes against the fix
(verified independently: reverting the guard fails exactly these 5 tests
with DivideByZeroException; restoring it passes). Confirmed to reduce
SqlServer/Postgres failures from 464 to 341, stable across two runs on both
providers, with no SQLite regression.
GetBacklinkPagesPaged unioned three page sets (backlinks, outlinks,
second-order links) instead of mirroring its own SQLite reference
script (GetBacklinkPagesPaged.sql), which only ever selects true
backlinks (PR.ReferencesPageId = @pageID). This made a page with an
outlink but zero real backlinks incorrectly report its outlink target
as a page linking to it, causing TestBacklinks_* golden-file failures
on SqlServer/Postgres providers.

Removes the outlink and second-order-link branches and their
client-side ID union, leaving only the true-backlinks query. Adds a
regression test covering the "outlink but no backlinks" scenario.

Known follow-up (not fixed here): GetRelatedPagesPaged has an
analogous scope bug in the opposite direction - its own SQLite
reference (GetRelatedPagesPaged.sql) is a 3-way UNION of
backlinks+outlinks+second-order links, but the current EF Core
implementation is backlinks-only. Left as a separate future task.
… mirror SQLite reference script

GetSimilarPagesPaged.sql computes similarity as (Count(0) / (SELECT COUNT(0) ...)) * 100.0
with two INTEGER operands, so SQLite truncates via integer division *before* the `* 100.0`
multiply promotes the result to REAL (e.g. 1/4 -> 0, not 0.25). The EF implementation cast
totalRootTagCount to double before dividing, which computes the mathematically "correct"
floating-point percentage instead — but that's not what the reference script produces, and
the golden-file regression tests are generated against the reference behavior. Removing the
(double) cast replicates the same truncating integer division as SQLite.

Verified by tester: TestSimilar_* went from 72 failed to 0 failed, and the full EF Core test
suite went from 196 failed to 124 failed, on both SqlServer and Postgres providers, with no
side effects outside this scope.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ze=0 (DivideByZeroException)

GetAllPublicProfilesPaged computed PaginationPageCount via C# integer
division ((filtered.Count + (effectivePageSize - 1)) / effectivePageSize)
with no guard against effectivePageSize=0, so SqlServer/Postgres threw
DivideByZeroException whenever markup passed an explicit pageSize of 0 -
reachable from real wiki markup via ##ProfileList(0) / ##ProfileGlossary(0).
Same bug class as 5ef0d2fa (EfPageRepository), but in EfUsersRepository,
where the division runs entirely in-memory over an already-materialized
list rather than being translated to SQL. SQLite/Dapper never hit this
because LIMIT 0 returns zero rows before any division happens.

Adds a pageSize == 0 ? 0 : ... guard at this one call site, replicating
the SQLite LIMIT-0 semantics (zero pages), plus
UsersRepositoryPaginationEdgeCaseRegressionTests.cs reproducing the crash
against the old code and passing against the fix.

Note: EfUsersRepository.cs has 5 more unguarded occurrences of the
identical division pattern (GetRolePermissionsPaged, GetRoleMembersPaged,
GetAccountPermissionsPaged, GetAccountRoleMembershipPaged,
GetAllUsersPaged), none reachable from ##ProfileList/##ProfileGlossary -
left as-is, candidates for a future separate fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nks+2nd-order union

Mirrors its own SQLite reference (GetRelatedPagesPaged.sql), which is a three-way
UNION of backlink, outlink, and second-order-link page-reference branches. The prior
EF Core translation only ever selected true backlinks (pr.ReferencesPageId == pageId),
identical to GetBacklinkPagesPaged's own scope - so a page with a real outlink but no
real backlinks was reported as having zero related pages, unlike the SQLite provider.

Each branch is resolved via its own translatable query and combined/deduplicated
client-side (no single-query LINQ translation for the reference's UNION + COUNT(*)
OVER() exists), following the same Contains(...) temp-table pattern used elsewhere
in this repository.

Note: after this fix, GetBacklinkPagesPaged (backlinks-only, narrow) and
GetRelatedPagesPaged (union of backlinks/outlinks/2nd-order, wide) have naming that
suggests the opposite of their actual scope - confusing, but each faithfully mirrors
its own SQLite reference script.

Adds PageRepositoryRelatedScopeRegressionTests, a scope regression test asserting
GetRelatedPagesPaged surfaces an outlink target even with zero backlinks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…er.cs

SeedPageFileAttachments used Dictionary key strings ("{p.Name} {p.Namespace}"
and "{pageId} {navigation}") where the separator character was an embedded
NUL byte (0x00) instead of a literal space, on 4 lines (731, 732, 737, 743).
This was introduced directly in a71ee1a, not a later corruption.

Text-search tools detected the file as binary because of these NUL bytes.
Since both sides of every key comparison used the same NUL separator,
matching was symmetric and there was no functional impact - confirmed
byte-for-byte (exactly 4 NUL bytes, isolated to this one file out of 525)
and via strict A/B testing (old vs new produce an identical set of failures
across the full test suite).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t appsettings.json

Hardcoded Windows backslash paths (MarkupPath, OriginalDatabasePath,
DatabasePath) failed to resolve on ubuntu-latest CI runners. Replaced
with Path.Combine and forward-slash literals that work on both platforms.
… on EF providers

Reference SQLite seed data's Page.ModifiedDate and PageRevision.ModifiedDate
columns routinely diverge (e.g. an unrelated metadata refresh can touch
Page.ModifiedDate without editing the page's content). The EF seed pipeline
was collapsing both onto Page.ModifiedDate, so PageRevision.ModifiedDate was
wrong on SqlServer/Postgres and broke ##RecentlyCreated/##RecentlyModified
ordering (which reads PR.ModifiedDate, see GetTopRecentlyModifiedPagesInfo.sql).

Add PR.ModifiedDate as RevisionModifiedDate to the seed-generation SQL and a
matching TwDefaultWikiPage.RevisionModifiedDate property, then have both EF
providers assign PageRevision.ModifiedDate from RevisionModifiedDate instead
of ModifiedDate. Regenerated Seed/tightwiki.seed.zip via GenerateSeedData.
CI-environment fix only, no production code touched. Unlike windows-latest,
ubuntu-latest runners don't ship en_US.UTF-8 ICU/locale data by default, so
CultureInfo.CurrentCulture for the dotnet test process falls back to
invariant-culture formatting instead of en-US. Several *.wiki.expected
fixtures under TightWiki.Tests/Markup embed DateTime.ToShortDateString()/
ToShortTimeString() output generated on an en-US environment, so the
missing locale caused pure formatting mismatches (not content differences)
against those fixtures on the Postgres job.

Adds a "Set up en_US locale" step (locale-gen + update-locale) and sets
LANG/LC_ALL=en_US.UTF-8 as env for the Run Tests step, so the ambient
culture on ubuntu-latest matches what the fixtures were generated with.
…of OS/CI locale

Markup/*.wiki.expected fixtures embed culture-sensitive formatting (e.g.
DateTime.ToShortDateString()/ToShortTimeString(), upsize's "{fontSize:F1}rem")
produced by plugin code that intentionally isn't hardcoded to a culture - it's
meant to respect the site's request culture in production via
app.UseRequestLocalization. Since these tests call WikiEngine directly, nothing
sets that culture, so it fell back to whatever the OS/CI runner exposed to the
process - en-US on windows-latest, invariant/other on ubuntu-latest, cs-CZ on a
Czech dev machine - causing spurious failures depending on where the suite ran.

The previous fix (054744b) tried to paper over this in CI only, by
locale-gen'ing en_US.UTF-8 and setting LANG/LC_ALL on the ubuntu-latest
Postgres job - but that only ever addressed one runner and didn't actually
change the ambient culture .NET resolves for the test process, so it didn't fix
the underlying issue.

Add TightWiki.Tests/TestCultureInitializer.cs, a [ModuleInitializer] that pins
CurrentCulture/CurrentUICulture and DefaultThreadCurrentCulture/
DefaultThreadCurrentUICulture to en-US before any other code in the test
assembly runs, independent of the host OS/CI locale. This covers every thread
xunit may run plugin code on, without touching any production/localized code
path. Remove the now-unneeded locale-gen step and LANG/LC_ALL env from the
ubuntu-latest Postgres job in Regression Tests.yml.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ageTests.cs

ICU versions differ between the Windows runtime used to generate the
Markup/*.wiki.expected fixtures and the ICU bundled with .NET on
ubuntu-latest. DateTime.ToShortTimeString() (used by HistoryFunctions.cs
and MetadataFunctions.cs) separates the time from the AM/PM designator
with a plain space (U+0020) on Windows, but with a narrow no-break space
(U+202F) on newer ICU per a CLDR formatting change. The two are visually
indistinguishable but compare as different strings, causing spurious CI
failures on ubuntu-latest that don't reproduce locally on Windows.

Normalize only this one known character in the test comparison before
Assert.Equal. Production code/output is untouched - end users still see
whichever character their runtime's ICU actually produces.
…rage

The Regression Tests badge in the README intro still pointed at
NTDLS/TightWiki (upstream), which doesn't run this fork's SQL
Server/Postgres CI legs at all - point it at eMukator/TightWiki instead so
it reflects this repo's own 3-way matrix status. Also note in the fork
callout that all three providers now run the same regression suite in CI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@eMukator

eMukator commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

@NTDLS
Thanks for taking a look!

My fault, I created a pull request only for commit a4762d6 "Extracted translations into a new TightWiki.SharedResources project to speed up builds" and forgot to switch branch after that. So now you see all commits that belong to TightWiki with Entity Framework + two drivers (SqlServer, Postgres). Obviously, I used vibe coding with Claude Code so it's on you if you want to add it to the main branch of TightWiki.

Answering in order:

Build modes — yes, exactly that. It's a build-time choice via the MSBuild property DataProvider (Sqlite | SqlServer | Postgres), not a runtime toggle:

dotnet build .\TightWiki -p:DataProvider=SqlServer
dotnet publish .\TightWiki -c Release -p:DataProvider=Postgres --runtime linux-x64 --self-contained false

Defaults to Sqlite if you don't pass it. One gotcha: after switching -p:DataProvider=, run dotnet restore again (not --no-restore) — which project reference gets pulled in (SQLite/Dapper vs. the EF Core driver) is resolved at restore time.

Generating the database snapshot — you don't need to. Seed/tightwiki.seed.zip already ships in the repo; a SQL Server/Postgres build reads its initial data (config, themes, default pages, emoji, menu items) from it automatically on first run against an empty database — schema migrations and seeding both happen at startup, no manual step. That file only gets regenerated by a maintainer if the reference content itself changes (GenerateSeedData.bat, part of Release.Build.bat) — it's a dev-time tool, never something an end user or deployer runs.

Configuring SQL Server — one connection string, ConnectionStrings:TightWikiEfCore, covers everything (all 8 schemas + ASP.NET Core Identity):

"ConnectionStrings": {
"TightWikiEfCore": "Server=(localdb)\mssqllocaldb;Database=TightWiki;Trusted_Connection=True;"
}

If you're in Visual Studio, there's actually a Debug-SqlServer configuration already in the Configuration dropdown that works out of the box against LocalDB — appsettings.Development.json ships a working connection string for it, so picking it + F5 is the fastest way to try it.

The VS "binary file" dialog on SqlServerDatabaseManager.cs — good catch, that's a real bug, not something specific to your setup. The file had 4 stray embedded NUL bytes (dating back a while), which is exactly what makes editors/git/ripgrep treat a text file as binary. Fixed it in the batch I just pushed: 9592506 (eMukator@9592506) — pull latest main and it should open cleanly.

Also worth noting since it's relevant to trying this out: this push includes a fairly large pass making the SQL Server/Postgres seed data byte-for-byte parity with the SQLite reference (same IDs, timestamps, view counts, etc.) plus a full cross-provider regression test suite wired into CI — so both providers should now behave identically to the SQLite version, verified test-by-test, not just "it starts up."

One more thing since I wrote the above — the full regression suite (~1665 tests, repository-level + full-page rendering) now passes cleanly in CI across all three providers (SQLite, SQL Server, PostgreSQL), matrix-tested on both windows-latest and ubuntu-latest. So the SQL Server/Postgres support isn't just "it starts up" at this point — it's been verified test-by-test against the same golden-file suite SQLite has always run, including admin bootstrap, search/backlinks, page history, and PostgreSQL specifically also running Linux CI now for the first time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants