Extracted translations - #99
Conversation
…o speed up builds
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.
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>
|
@NTDLS 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 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": { 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. |
Hi @NTDLS, I made a few small adjustments that I hope will speed up the build.