Repository navigation
fix(dapper): match PostgreSQL provider names and quote identifiers via dialect hook - #258
Conversation
…rations
FluentMigrator 7.2 AddPostgres() uses Postgres15_0Processor, whose
DatabaseType is PostgreSQL15_0 and aliases are PostgreSQL15_0/PostgreSQL,
not Postgres. IfDatabase("Postgres") therefore skipped every create-table
branch and a fresh PG migrate failed at 10002.
Accept both names so older and 7.2 processors keep working, and add a
Testcontainers PostgreSQL fresh-DB migration test.
Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
IMigrationProcessor lives in the FluentMigrator namespace in 7.2, not FluentMigrator.Runner.Processors. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
IfDatabase(string[]) is an exact OrdinalIgnoreCase match against DatabaseType plus aliases, so listing only "PostgreSQL" misses PostgreSQL15_0 as a type and Postgres92 entirely. Switch the DateTimeOffset create-table branches to a predicate that matches SqlServer/Oracle/MySql exactly and any DatabaseType starting with Postgres (covers Postgres, PostgreSQL, PostgreSQL10_0/11_0/15_0, Postgres92, PostgreSQL92). Skip Create.Table when the table already exists so a database that reaches 10001/20001/20004 without VersionInfo rows (manual schema or a different runner) does not fail. Recorded VersionInfo versions still do not re-run. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
sfmskywalker
left a comment
There was a problem hiding this comment.
Code Review, Round 1/4: REQUEST_CHANGES + HIGH @ e1bc44a
The PG side of the diagnosis is right. With AddPostgres() (DatabaseType PostgreSQL15_0, aliases PostgreSQL15_0,PostgreSQL), IfDatabase("Postgres") never matches. The predicate does fix fresh PG migrations: all 15 versions and 13 tables on PG17. Two problems block merge, though. One is a new regression the predicate introduces for SQL Server. The other is CI: it is red on the PR's own fresh-PG test.
Blockers
B1: The predicate breaks SQL Server (and MySql/Oracle variants), the same bug moved to another provider.
IfDatabase(Predicate<string>)only receivesDatabaseType.IsDateTimeOffsetProviderexact-matches"SqlServer", but no FluentMigrator 7.2 SQL Server builder reports that DatabaseType:AddSqlServer()andAddSqlServer2016()registerSqlServer2016Processor, with DatabaseTypeSqlServer2016and aliasSqlServer.AddSqlServer2008/2012/2014()reportSqlServer20xx, also with aliasSqlServer.
- The old
IfDatabase("SqlServer", …)matched through the alias. The new predicate doesn't, andIfDatabase("Sqlite")doesn't match either. So on SQL Server neither branch runs: 10001, 20001 and 20004 are recorded with no WorkflowDefinitions, WorkflowInstances, Bookmarks, WorkflowExecutionLogRecords, ActivityExecutionRecords, WorkflowInboxMessages or BookmarkQueueItems tables. - This hits fresh installs, and also SQL Server DBs upgraded from before 3.3, which would skip BookmarkQueueItems. The workbench uses
AddSqlServer(). - The same happens for
AddMySql()/AddMySql8()(MySql8),AddMySql5()(MySql5),AddOracleManaged()(OracleManaged),AddOracle12C()(Oracle12c) andAddOracle12CManaged()(Oracle12cManaged). All of them matched before through theMySql/Oraclealiases. OnlyAddOracle()(Oracle) still matches. - Verified two ways:
- Resolving each builder's processor and comparing old vs new matching.
- Running the real migrations on PG through a processor that reports SQL Server's identifiers (
SqlServer2016+ aliasSqlServer). Base: OK, 13 tables. This PR:42P01 relation "public.WorkflowDefinitions" does not existat 10002, the exact #255 symptom.
DateTimeOffsetProvider_DoesNotChangeOtherProvidersassertsSqlServer2008,MySql8andOracle12c→ false. That locks the regression in: those processors did take this branch before.- Fix: keep the alias-aware
paramsoverload and just add the PG names. For example, a singleMigrationDatabases.DateTimeOffsetProviders = ["SqlServer", "Oracle", "MySql", "Postgres", "PostgreSQL", "PostgreSQL92"]used asIfDatabase(MigrationDatabases.DateTimeOffsetProviders). Every 7.2 PG processor hasPostgres,PostgreSQLorPostgreSQL92in DatabaseType or aliases. If you keep a predicate, it must prefix-match every family (SqlServer*,MySql*,Oracle*,Postgres*). Either way, fix the theory data. - Regression test: resolve the real processor for each builder the repo references (
AddSqlServer(),AddSQLite(),AddPostgres(); MySql/Oracle optional). Assert that exactly one of the two create branches applies, using FluentMigrator's real semantics (DatabaseType ∪ aliases forparams, DatabaseType only for a predicate). That test catches future provider renames in either direction.
B2: CI is red. The fresh-PG Testcontainers test fails in CI on this head.
- CI has Docker, so the test runs; it is not skipped.
ubuntu-latestresult:A fresh PostgreSQL DB applies every Dapper migration and can persist SerializedMetadata + AggregateFaultCount [FAIL],Npgsql.PostgresException : 42P01: relation "activityexecutionrecords" does not existatDapperPostgreSqlMigrationTests.cs:70(store.SaveAsync). Summary: Elsa.Persistence.Dapper.UnitTests Failed 1 / Passed 40, overallTest Failed // Failed: 1, Passed: 465, Skipped: 2. - Reproduced identically against a local PostgreSQL 17. The migrations succeed and every table exists. The failure is in the store:
- FluentMigrator's PG quoter (default
ForceQuote = true) creates"ActivityExecutionRecords". - The Dapper PG dialect emits unquoted identifiers, which PG folds to
activityexecutionrecords.
- FluentMigrator's PG quoter (default
- There is a second problem behind it. With
Force Quote=false, the next failure is23502: null value in column "id", becausePostgreSqlDialect.Upsertleaves the primary key out of the insert column list (fieldsexcludes the PK).SqlDialectBase/SqliteDialectdon't have this problem. - Fix, preferred: quote identifiers in the PG dialect/query builder, and include
primaryKeyFieldinPostgreSqlDialect.Upsert's insert list. This keeps FluentMigrator's defaults, and it keeps theSchema.Table(...).Exists()guards working.PostgresProcessor.TableExistscomparestable_namecase-sensitively, so withForce Quote=falsethe guards would silently miss the lowercased tables. - Alternative: narrow this PR's test to migration-only assertions and track the store fixes as a separate 3.9 blocker. Then say explicitly that Dapper + PostgreSQL still does not work end-to-end after this PR.
Non-blocking
- Already-broken PG DBs aren't healed.
- Released 3.6–3.8 (FluentMigrator 7.2 since 3.6.0) fresh PG installs stop with 10001 recorded and only
VersionInfopresent. 10001 never re-runs, so this PR still fails on them at 10002 (verified). - Deleting the row,
DELETE FROM "VersionInfo" WHERE "Version" = 10001;, then migrating up completes: all 15 versions and 13 tables (verified). - These DBs can't hold data, so a release note with that line is enough. An automated repair is optional and would need its own test.
- Released 3.6–3.8 (FluentMigrator 7.2 since 3.6.0) fresh PG installs stop with 10001 recorded and only
- The Exists guards are fine for normal upgrades.
- Recorded migrations never re-run, and supported providers (SQLite, SQL Server, PG) apply each migration transactionally, so a partially applied 10001 isn't a realistic state.
- Indexes are inline
.Indexed()within the guardedCreate.Table, and there are no separateCreate.Index/FK calls, so indexes stay consistent with their table. - Caveat: a table-exists check passes even when the columns differ, so schema drift is silently accepted. Consider at least a warning log.
- The guards cover only the IfDatabase tables.
Runtime/V3_1(20002) andIdentity/Initial(30001) still create tables unguarded, so "DBs without VersionInfo rows" is only partly handled. Either guard those too or narrow the rationale.
- Fail fast on an unknown provider. Unmatched providers silently create nothing and still record the version, which is what turned both #255 and B1 into silent failures. For example,
IfDatabase(t => !IsDateTimeOffset(t) && !IsSqlite(t)).Delegate(() => throw new NotSupportedException(...)), with the Sqlite name in the same helper so there is one source of truth. - Naming and docs.
IsDateTimeOffsetProviderdescribes the column type rather than the provider set;DateTimeOffsetProvidersorUsesDateTimeOffsetSchemawould read better. The XML doc listsPostgreSQLandPostgreSQL92as DatabaseType values, but they are aliases only. - FYI, out of scope: a full
MigrateDown(0)on PG fails inRuntime/V3_3.Down(Rename.Column("Id")…To("Key"), 42701). This also happens on the base branch.
Verification
- Tests:
- Elsa.Persistence.Dapper.UnitTests: 40/41 locally. The only failure is the Testcontainers test, because the box has no Docker.
- The same test pointed at local PG17 fails as in CI (42P01
activityexecutionrecords). - Elsa.Dapper.UnitTests: 16/16.
- Probes:
- SQLite: fresh migrate OK; base→20006→PR OK; base full→PR OK; down 20006/20005 and back up OK. All reach 15 versions and 13 tables.
- PG17 with
AddPostgres(): fresh OK; old DB at 20006 → PR OK; down 20006/20005 and back up OK.
- Revert checks:
- Restoring
IfDatabase("SqlServer","Oracle","MySql","Postgres")makes the fresh-PG test fail earlier, atMigrateUp(42P01 WorkflowDefinitions). That is correct revert-proofing, once B2 is resolved. - Removing the Exists guards makes
Replaying 10001/20001…fail (table "WorkflowDefinitions" already exists).
- Restoring
- Scope: all
IfDatabasecall sites are in the three changed files (10001, 20001, 20004). No other migration, including Identity 30001+, usesIfDatabase. The SQLite branch (IfDatabase("Sqlite")vs DatabaseTypeSQLite) is unchanged and correct.
…t test The predicate overload only sees DatabaseType, so AddSqlServer() (SqlServer2016), AddMySql() (MySql8) and AddOracleManaged() no longer matched. Switch back to IfDatabase(params string[]) with a shared DateTimeOffsetProviders list: SqlServer, Oracle, MySql, Postgres, PostgreSQL, PostgreSQL92. Assert the real processors from AddSqlServer/AddSQLite/AddPostgres/ AddMySql/AddOracleManaged take exactly one create branch. The fresh-PG Testcontainers test now asserts migrate + tables only. Quoting identifiers plus including the PK in PostgreSqlDialect.Upsert is a store-wide dialect change, not a focused migration fix. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
|
Addressing Round 1 REQUEST_CHANGES. There were no line-level review threads on this PR, so this is the reply for both blockers. B1 — SQL Server / MySql / Oracle regression. The predicate overload only sees New tests resolve the real processors from B2 — CI red on persist. The Testcontainers test now asserts migrate + expected tables/columns only. I did not quote identifiers in the PG dialect in this PR. Doing it properly means quoting every table/column the store emits ( Release note (verified by review): 3.6–3.8 PG DBs stuck at 10002 heal with |
FluentMigrator force-quotes PG table/column names. Re-implement ISqlDialect on PostgreSqlDialect so Dapper emits matching quoted identifiers, and include the primary key in ON CONFLICT upsert. Restore the fresh-PG Testcontainers test to migrate, persist, and run a WriteLine workflow to completion. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
Avoid interpolated-string quote escaping so the helper stays compile-safe. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
c5b218d to
23fcce2
Compare
SqliteDbConnectionProvider registers a string-only DateTimeOffset handler on SqlMapper. After persist, PG Find failed with InvalidCastException. Register a PG handler that accepts DateTime, DateTimeOffset, and string. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
IWorkflowHost SaveMany concatenates upserts. Without a trailing semicolon PostgreSQL reports 42601 at the second insert. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
sfmskywalker
left a comment
There was a problem hiding this comment.
Code Review, Round 2/4: REQUEST_CHANGES + HIGH @ 39ae028
The head moved twice during this review (23fcce2 → 9b14229 → 39ae028). I covered all three; the findings below are against 39ae028.
Both R1 blockers are resolved:
- B1: the alias-aware list is back. I verified it with real processors and with a spoofed-processor migration run.
- B2: CI is green. The fresh-PG Testcontainers test ran and passed: Dapper UnitTests 43/43, 0 skipped. I reproduced it on local PG17.
What still blocks: outside the narrow path the test exercises, most read paths of the Dapper PG store fail, including publish and run-by-definition. Dapper + PostgreSQL is still not usable, and "Fixes #255" overstates what lands.
Blocker
B3: Core PG store paths still fail on a fresh, fully migrated PG DB.
I ran 90 store and runtime operations through the real Dapper stores against PG17 at this head, and 36 fail. 9 of them also fail on SQLite, so they are pre-existing (see FYI). That leaves 27 PG-only failures, all 42703 column "<lowercased>" does not exist:
- Management:
IWorkflowDefinitionPublisher.PublishAsync.IWorkflowDefinitionService.FindWorkflowGraphAsync(id, VersionOptions.Published), which is the run-by-definition lookup.FindAsyncwith anyVersionOptionsflag (Latest, Published, LatestOrPublished, Draft).TryUpdateLatestAsyncandFindLastVersionAsync.FindSummariesAsync/FindManyAsyncwhen paged (the Studio definitions list, default orderCreatedAt), when ordered, and withSearchTerm.
- Instances:
- Paged
FindManyAsync. SummarizeManyAsync(the Studio instance list), with and without order or search.- The
BeforeLastUpdatedfilter. TryMarkInterruptedAsync.
- Paged
- Runtime:
BookmarkQueueStore.PageAsync, used by the queue processor.- Paged
BookmarkStoreandTriggerStorefinds. - Ordered
ActivityExecutionStore.FindManyAsync. - Journal reads:
WorkflowExecutionLogStore.FindAsync(order)andFindManyAsync(page).
These pass:
- The migrations.
- Upsert, insert, update and delete by key, and
FindAsyncby Id. - Unordered filters.
- The identity stores.
- WriteLine and Event-bookmark workflows run via
IWorkflowHost.
Causes: none are in the dialect.
ParameterizedQueryBuilderExtensionsinlines the field name inOrderBy(string)andOrderBy(OrderField[])(neither callsISqlDialect.OrderBy). It does the same inIs(VersionOptions)(and IsLatest = 1), both search-term helpers,LessThan,StartsWith, and the pagedDelete(table, pk, inner).DapperWorkflowInstanceStore.TryMarkInterruptedAsyncappends the rawand not Status = @FinishedStatus.- For VersionOptions, quoting alone is not enough:
"IsPublished" = 0fails on PG with42883 operator does not exist: boolean = integer.
Fix (preferred; only PG behavior changes): add a dialect hook and route those call sites through it.
ISqlDialect: addstring QuoteIdentifier(string identifier) => identifier;andstring BooleanLiteral(bool value) => value ? "1" : "0";as default interface methods. For zero public-API change, use an internal interface that onlyPostgreSqlDialectimplements instead.PostgreSqlDialectreturnsQuote(...)andtrue/false.- Builder: call
query.Dialect.QuoteIdentifier(...)in bothOrderByoverloads,LessThan,StartsWith, both search-term helpers and the pagedDelete.Is(VersionOptions)uses the quoted names plusBooleanLiteral. - Store: in
TryMarkInterruptedAsync, use$"and not {q.Dialect.QuoteIdentifier("Status")} = @FinishedStatus".
I prototyped exactly this on this head: +25/−13 across 4 files.
- All 27 PG-only failures pass. Publish, run-by-definition, both Studio lists, the journal and the bookmark queue all work, and the remaining PG failures are the same set as on SQLite.
- The identity defaults leave SQLite and SQL Server SQL byte-identical. SQLite probe results are unchanged.
- Dapper UnitTests pass 43/43, including the fresh-PG test on local PG17.
Tests to add with the fix:
- A golden-SQL test showing that SQLite/SQL Server builder output is unchanged.
- In the fresh-PG test: publish followed by
FindWorkflowGraphAsync(Published), a pagedFindSummariesAsync(VersionOptions.Latest),SummarizeManyAsyncwith an order, and a journalFindManyAsync(page).
Fallback, if shared files stay off-limits for 3.9:
- Merge as a migrations plus partial-store fix, and change "Fixes #255" to "Refs #255".
- Say in the PR and the release notes that the Dapper + PostgreSQL store is not functional in 3.9: publish, lists and version lookups fail.
- Open a 3.10 issue with the list above.
Conditions
-
C1: clean. None of these changed:
SqlDialectBase,ISqlDialect,SqliteDialect,SqlServerDialect,ParameterizedQueryBuilderExtensions,Store, the module stores, the SQLite/SQL Server providers, orTypeHandlers/Sqlite.- Outside
PostgreSqlDialect.cs, the migrations and the tests, two things changed.PostgreSqlDbConnectionProvidergained a static ctor that registers a DateTimeOffset handler, and there is a newTypeHandlers/PostgreSql/DateTimeOffsetHandler.cs. Both are PG-only code, but the handler registration is process-global (see non-blocking). - Packages:
Directory.Packages.propsonly addsPackageVersionrows forFluentMigrator.Runner.MySql/Oracle/Postgres. Only the test csproj references them, and it hasIsPackable=false. - The Migrations csproj only adds
InternalsVisibleTo. Shipped dependencies are unchanged. - A
-wdiff of the migrations shows only theIfDatabaseargument and theExists()wrappers. No columns changed.
- Outside
-
C2: met at this head.
- CI
ubuntu-latestpassed. Dapper UnitTests: 43/43 passed, 0 skipped. Overall: 469 passed, 2 skipped in other assemblies. - 23fcce2 failed with
Error parsing column 11 (StartedAt … DateTime): the SQLite handler leaked into the PG test. 9b14229 fixed that. - 9b14229 then failed with
42601 syntax error at or near "insert": a multi-rowSaveManyAsynchit the unterminated upsert. 39ae028 fixed that. - On local PG17 the test passes both on its own and with the full assembly.
- CI
-
C3: met.
DapperPostgresProcessorNameTestsresolves the real processors forAddSqlServer/AddSQLite/AddPostgres/AddMySql/AddOracleManagedthrough DI; none of the names are hand-typed. For the spoof probe I ran the real migrations on PG17 through a processor reportingSqlServer2016with aliasSqlServer. Base and this head both reach 15 versions and 13 tables. -
C4: I agree with the unsupported position. A working hand-made lowercase DB realistically does not exist:
- The PG upsert has left the PK out since the dialect was added to elsa-core in Sep 2023 (
cd2591430/72d855090, 3.0), and the builder has always left the PK out offields. So everySaveAsyncon PG failed with23502in 3.0–3.8, lowercase or not. - FluentMigrator's PG quoter force-quotes, so migration-created schemas were always mixed-case, and the unquoted store got
42P01. - A working lowercase setup would have needed both a hand-made schema and a custom dialect.
The rename path in the PR body is incomplete, though:
- FluentMigrator's
TableExistsis case-sensitive. Running the migrations against lowercase tables that were not renamed creates empty mixed-case duplicates next to them. - Renamed tables without matching
VersionInforows fail at 10002+ (Alter.Table…AddColumn, the column already exists) or at 20002/30001 (unguarded creates).
Suggested release-note line: "Dapper + PostgreSQL now uses the FluentMigrator-created, quoted mixed-case schema. Hand-made lowercase/unquoted schemas are not supported. They never worked with the Dapper PG store, because every upsert failed. Recreate the schema by running the migrations on an empty database and re-import any data. For installs stuck after 10001 on 3.6–3.8, run
DELETE FROM "VersionInfo" WHERE "Version" = 10001;and restart." - The PG upsert has left the PK out since the dialect was added to elsa-core in Sep 2023 (
Non-blocking
- Maintainability.
PostgreSqlDialectre-implements 14ISqlDialectmembers and duplicates theSqlDialectBasetemplates.Delete/Count/IsNull/IsNotNull/Insert/Updateare non-virtual in the base. A call through aSqlDialectBase- orPostgreSqlDialect-typed reference gets unquoted SQL; a call throughISqlDialectgets quoted SQL. That works today because every caller usesISqlDialect, but it is a trap.- A
protected virtual string QuoteIdentifier(string)(identity by default), used by the base templates, would shrink the PG dialect toQuoteIdentifierplusUpsert, with byte-identical SQLite/SQL Server output. - If B3 takes the hook route, fold this in now, since it is the same hook. Otherwise do it in 3.10.
- DateTimeOffset handler. Dapper type handlers are process-global, and the last one registered wins. If a process initializes
SqliteDbConnectionProviderafterPostgreSqlDbConnectionProvider, PG reads break again, which is the 23fcce2 CI failure. Single-provider apps are fine. For the tests, put the PG test in its own non-parallel collection, or share one tolerant handler between providers (3.10). - Revert-proofing.
- Each of these reverts fails the fresh-PG test: restoring the base dialect (
42P01 activityexecutionrecords), dropping the upsert;(42601), dropping the PK from the upsert (23502). - Dropping
PostgreSQL/PostgreSQL92from the list fails 4 processor tests plus the fresh-PG test. - Gap: the processor test checks the constant, not the migrations. If a call site inlines a different list, nothing catches it for SQL Server/MySQL; only PG is caught, by the container test. A Docker-free test would close this: run the real migrations through a processor reporting
AddSqlServer()'s DatabaseType and aliases, and assert that all tables exist.
- Each of these reverts fails the fresh-PG test: restoring the base dialect (
- R1 items.
- Done: release note (in the PR body); naming and XML doc.
- Not done, all optional: a drift warning; guards for 20002/30001 (or narrow the rationale); failing fast on an unknown provider.
- FYI, pre-existing on every provider (same on SQLite; worth separate issues):
DapperKeyValueStoreuses the tableKeyValues, but the migrations createKeyValuePairs, so the whole KV store fails.BookmarkQueueItemRecord.SerializedOptionshas no column, soBookmarkQueueStore.SaveAsync/AddAsyncfail.CountDistinctAsyncemits twoselects.- The paged
Store.DeleteAsyncdoes not copy the inner query's parameters.
Verification
- Tests: Dapper UnitTests 42/43 locally; the only failure is that there is no Docker. 43/43 with the fresh-PG test pointed at local PG17. Elsa.Dapper.UnitTests 16/16.
- PG17 migrations:
- Fresh: 15 versions and 13 tables.
- Down to 20005 and back up: OK.
- A stuck 3.6–3.8 DB (base fresh stops at 10001) is not healed by this head. The
DELETEplus migrate heals it.
- SQLite: fresh; base@20006 → head; base full → head; down to 20005 → up. All OK, each ending at 15 versions and 13 tables.
Route every identifier ParameterizedQueryBuilderExtensions inlines through ISqlDialect.QuoteIdentifier. The default is a no-op so SQLite, SQL Server, MySQL and Oracle SQL stays byte-identical; only PostgreSqlDialect double-quotes. Snapshot tests lock the pre-hook non-PG SQL. The fresh-PG Testcontainers test now also covers list, search, OrderBy, VersionOptions and paged delete. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
VersionOptions compared boolean columns to 1/0, which PostgreSQL rejects (42883). ISqlDialect.BooleanLiteral defaults to 1/0 so non-PG SQL stays byte-identical; only PostgreSqlDialect emits true/false. The fresh-PG test now covers publish, run-by-definition, VersionOptions, Studio lists, journal and activity-execution reads, bookmark-queue paging, and paged delete. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
3.9-only BeforeLastUpdated uses LessThan, which still inlined the column name. Route it through QuoteIdentifier so PG quotes and SQLite/SQL Server SQL stays byte-identical. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
|
Addressing Round 2 REQUEST_CHANGES B3. There are no line-level review threads. B3 — unquoted identifiers and integer booleans on shared builder paths. Landed in eec9581 (BooleanLiteral + QuoteIdentifier routing) on top of the earlier QuoteIdentifier hook.
Non-test size for this increment: 4 files, +25 / −6. No non-PG behaviour change. |
The slim Testcontainers host never called AddActivitiesFrom or IActivityRegistryPopulator, so PublishAsync NRE'd in CreateActivity when rematerializing the draft. Store SQL is unchanged. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
AndWorkflowInstanceSearchTerm historically inlined unquoted ID, which is byte-identical on SQLite/SQL Server. Quoted on PG it is 42703 against FluentMigrator's "Id". Non-PG SQL is unchanged. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
sfmskywalker
left a comment
There was a problem hiding this comment.
Code Review, Round 3/4: REQUEST_CHANGES + HIGH @ 8526e90
Addendum, latest push
3a58d186(arrived while this review was being finalised): B4 is addressed, but viaquery.Dialect.QuoteIdentifier("Id") == "Id" ? "ID" : "Id". That produces correct SQL, but it sniffs the dialect's behaviour to keep one historical token, and a reader has to reverse-engineer why. Please replace it with the plainquery.QuoteIdent("Id")as described under B4. The one-token snapshot change (IDtoId) is harmless on SQLite and on default-collation SQL Server, and it fixes case-sensitive SQL Server collations; I'm asking the release lead to sign off on it explicitly. The new PG shape assertions are good; keep them. B5 (a) and (b) are not in that push, so the fresh-PG test is still expected to fail. The next round is the last one in the review loop (4/4), so please address B4 (plain form) and B5 together.
The head moved during this review, from eec9581 to 8526e90. The new commit only touches the test: it populates the activity registry and registers WriteLine before publishing. The non-test code is byte-identical at both SHAs, so every runtime check below applies to 8526e90. CI results are for 8526e90.
R2's B3 is mostly fixed, and the hook design is what we asked for:
QuoteIdentifier/BooleanLiteraldefault to the identity and1/0. OnlyPostgreSqlDialectoverrides them.- Every inlined identifier in the builder goes through the hook, and so does
TryMarkInterruptedAsync. - SQLite and SQL Server SQL is byte-identical to
release/3.9.0. - 26 of R2's 27 PG-only failures now pass.
Two things block merge. The fix emits one wrong identifier, which breaks PG instance search. And the fresh-PG test is red in CI: it cannot pass as written.
Blockers
B4: Instance search emits "ID" on PostgreSQL, which fails with 42703.
AndWorkflowInstanceSearchTermpasses the literal"ID"throughQuoteIdentifier. Quoted PG identifiers are case-sensitive and the column is"Id", so anyWorkflowInstanceFilter.SearchTermfails with42703: column "ID" does not exist. That covers Studio instance-list search (SummarizeManyAsync) andFindManyAsync.- This is the current CI failure:
DapperPostgreSqlMigrationTests.cs:242. - The 90-operation run on PG17 at this head fails only this operation beyond the SQLite baseline (details below).
- Fix: use
query.QuoteIdent("Id"), the same spelling the definition-search helper already uses.- Side effect: the SQLite/SQL Server
instance-searchsnapshot changes one token (ID→Id). On SQLite, MySQL and Oracle, and on SQL Server with its default case-insensitive collation, the two are equivalent. - On a SQL Server database with a case-sensitive collation,
IDdoes not resolve to the migration-createdIdcolumn today, so this is a latent fix, not a regression. - Update the snapshot with a one-line comment. Call out the token in the PR body, because it is the only exception to "byte-identical" and the release lead should sign off on it explicitly.
- Side effect: the SQLite/SQL Server
- Add a PG shape assertion to
QueryBuilder_QuotesInlinedIdentifiers:Assert.Contains("\"Id\" like @SearchTerm", queries["instance-search"])andAssert.DoesNotContain("\"ID\"", queries["instance-search"]). - Verified: with only this change, PG17 failures equal the SQLite baseline exactly (81/90 on both).
B5: The fresh-PG test cannot pass as written, and ubuntu-latest is red.
The test ran in CI; it was not skipped. It failed on eec9581 with an NRE at PublishAsync: the activity registry was never populated, which 8526e90 fixes. On 8526e90 it fails on B4. I ran the whole chain against a local PG17, and two more failures sit behind B4:
- (a) Wrong status value. The
rec-pg-1activity record, at line 104, hasStatus = "Finished".ActivityStatushas noFinished, so the orderedIActivityExecutionStore.FindManyAsync(orderedActivities) throwsArgumentException: Requested value 'Finished' was not found. Use"Completed". - (b) Paged delete (line 351).
Store.DeleteAsync(filter, pageArgs, orderFields)drops the inner query's parameters. This is the pre-existing all-provider bug being filed separately; SQLite fails the same way with "Must add values for the following parameters".- On PG, the unbound
@DefinitionIdparses as a prefix operator on an unquoted identifier:42703: column "definitionid" does not exist. So this assertion cannot pass without fixing that bug. - To keep coverage of the PG-specific part (quoted
"Id" in (…)around a quoted, ordered, paged subquery) without changing other providers, build the statement in the test and merge the inner parameters:Keep thevar provider = new PostgreSqlDbConnectionProvider(_connectionString); var inner = provider.CreateQuery() .From("WorkflowInstances", "Id") .Is(nameof(WorkflowInstanceRecord.DefinitionId), "def-pg-page") .OrderBy(new OrderField(nameof(WorkflowInstanceRecord.CreatedAt), OrderDirection.Descending)) .Page(PageArgs.FromRange(0, 1)); var delete = provider.CreateQuery().Delete("WorkflowInstances", "Id", inner); delete.Parameters.AddDynamicParams(inner.Parameters); // Store.DeleteAsync drops these today (tracked separately) using var connection = provider.GetConnection(); Assert.Equal(1, await connection.ExecuteAsync(delete.Sql.ToString(), delete.Parameters));
remainingassertion after it. I checked this pattern on PG17 and on SQLite, and it deletes exactly one row on both. - Fixing
Store.DeleteAsyncitself (deleteQuery.Parameters.AddDynamicParams(selectQuery.Parameters)) is one line and emits the same SQL. But it changes SQLite/SQL Server behavior (from throwing to working), so leave it to the separate issue unless the release lead opts in.
- On PG, the unbound
- Verified: with B4 + (a) + (b) applied locally, the test passes on PG17.
Revert-proofing
All runs used the test with B4, (a) and (b) applied locally, and a full rebuild each time:
- Revert PG
QuoteIdentifierto return the name unchanged: the test fails with42703: column "name" does not exist. - Revert PG
BooleanLiteralto1/0: the test fails with42883: operator does not exist: boolean = integer. - Make
SqlDialectBase.QuoteIdentifierquote: the SQLite and SQL Server snapshot tests fail, along with the no-op test. Making the defaultBooleanLiteralemittrue/falsealso fails them.
Release-lead bar
- Hooks only: met. The builder changes only through
QuoteIdent/BoolLit, which route toISqlDialect. OnlyPostgreSqlDialectoverrides. The only store-level change isTryMarkInterruptedAsync, also through the hook. - Byte-identical, captured from base: met at this head. I ran all 18 snapshot queries on
release/3.9.0withSqliteDialectandSqlServerDialect. The output is identical to the snapshots and to this head's output.- Coverage: both
OrderByoverloads, everyVersionOptionsflag, both search helpers,LessThan,StartsWith, paged delete, booleans, from/and/count/upsert. - The repo has only the SQLite, SQL Server and PostgreSQL dialects; there are no MySQL/Oracle dialect classes.
- B4 will change one token here, see above.
- Coverage: both
- Fresh-PG coverage: not met.
- The list is complete: publish →
FindWorkflowGraphAsync(Published), list/search/OrderBy, VersionOptions, paged delete, both Studio lists,BookmarkQueueStore.PageAsync, the journal,BeforeLastUpdated,TryMarkInterruptedAsync. But the test is red (B5). - Most assertions check returned data; the queue page asserts order, for example. Three are weak, see N4.
- The list is complete: publish →
- Release notes: met. The rename-path caveat (empty duplicate
"WorkflowDefinitions", matchingVersionInforows, recreate and re-import) andDELETE FROM "VersionInfo" WHERE "Version" = 10001;are both in the body. - R2 holds: met.
- Migrations, the migrations csproj, packages, the PG provider and the handler are byte-identical to R2's head (39ae028).
- The alias list is intact. Processor-name and migration tests pass 18/18.
- Running the same 90 operations on SQLite gives identical results on
release/3.9.0and on this head.
Non-blocking
- N1: Dialect duplication and the base-typed trap (recommended in this round).
PostgreSqlDialectstill re-implements 14 members with its ownQuote(...). MeanwhileSqlDialectBasenow has a virtualQuoteIdentifierthat its own templates don't use. The result is two parallel quoting mechanisms.Delete,Count(both overloads),IsNull,IsNotNull,InsertandUpdate(both overloads) are non-virtual in the base. Sonew PostgreSqlDialect().Delete("Bookmarks"), or any call through aSqlDialectBase/PostgreSqlDialect-typed reference, returns unquoted SQL.- Not a blocker: every caller in the repo goes through
ISqlDialect(ParameterizedQuery.Dialect,IDbConnectionProvider.Dialect), so nothing is broken at runtime. - But the fix is now cheap and guarded. Route the base templates through
QuoteIdentifier(...); with the identity default, SQLite and SQL Server stay byte-identical, and the snapshot test proves it.PostgreSqlDialectthen shrinks toQuoteIdentifier,BooleanLiteral,Upsert(PK plus;) and theCOUNT(distinct …)expression handling. - Do it now since another push is needed anyway, or open a 3.10 issue.
- N2: Two definitions of each default.
ISqlDialecthas default interface methods, andSqlDialectBasere-declares both as virtuals.- The snapshot tests only exercise the base virtuals. I flipped the
ISqlDialectdefault to quoting and all three snapshot tests stayed green. - Either drop one definition, or pin the interface defaults with a test on a stub that implements
ISqlDialectdirectly (Elsa.Dapper.UnitTestsalready hasStubSqlDialect).
- N3: PG
DateTimeOffsethandler and static ctor: PG-only, justified, no risk to SQLite/SQL Server.- The handler is internal and registered only from
PostgreSqlDbConnectionProvider's static ctor. That ctor runs only when a PG provider is instantiated;DapperFeature's default factory is SQLite, so SQLite- and SQL Server-only apps never touch it. - Dapper keeps one handler per type, and the last registration wins.
- PG registered after SQLite: SQLite reads go through the PG handler. Its
stringbranch is the sameDateTimeOffset.Parse(s)call, andSetValueis the sameparameter.Value = value. SQL Server returnsDateTimeOffset, which passes straight through; the SQLite handler would have thrown there, so PG-last is strictly more tolerant. - SQLite registered after PG: PG reads break again, exactly R2's 23fcce2 failure. That only happens in a process that instantiates both providers, and it affects only PG.
- Optional hardening: have
SqliteDbConnectionProviderregister the same tolerant handler, which is identical for strings. In the test assembly the order is timing-dependent today; it is fine in CI because Testcontainers startup makes SQLite register first.
- The handler is internal and registered only from
- N4: Test quality.
- The single
[Fact]runs about 30 checks in sequence, so the NRE hid B4, (a) and (b). Split them into facts over a shared migrated-DB fixture. - Add an environment-variable connection-string override so the test can run without Docker. That is how I ran it locally, and it would have caught all of this before push.
- Tighten three assertions: the Studio instance list (
TotalCount >= 1; assert ids and order with two or more rows), the Studio definitions list (Contains; assert order and page size), and the journal page (one row cannot showdescorder). - Also cover
VersionOptions.Draftand the journal'sFindAsync(order), and assert thatTryMarkInterruptedAsyncrefuses a Finished row.
- The single
- N5: Small cleanups.
- The builder's
OrderBy(string, …)could callquery.Dialect.OrderBy(...); the output is byte-identical and PG already overrides it. - A misindented
///line sits in thePostgreSqlDialectremarks. - PR body: "The store fix is not PostgreSQL-dialect-only" reads as if other providers change. Say instead that the hooks live in shared code, only PG overrides them, and other providers' SQL is byte-identical (plus the B4 token).
- The builder's
- FYI, pre-existing, out of scope:
StartsWithemits@SearchTermLikebut binds@{field}, so the KVStartsWithquery will fail on every provider even after theKeyValues/KeyValuePairstable fix. Add it to the separate issue with the four known items.
Verification
- Build and unit tests. Release build of both SHAs.
- Elsa.Persistence.Dapper.UnitTests: 46/47; the fresh-PG test is the only failure, as in CI. Elsa.Dapper.UnitTests: 16/16.
- I ran the fresh-PG test against a local PostgreSQL 17 cluster (the box has no Docker), with a local-only patch that swaps the Testcontainers connection string for a fresh database. Results: eec9581 NRE at publish; 8526e90
42703 "ID".
- 90-operation store/runtime run on PG17 (R2's checklist, real Dapper stores plus IWorkflowHost/publisher end-to-end):
- PG17: 80/90. The 10 failures are the 9 SQLite-baseline items plus
SummarizeManyAsync(SearchTerm)(B4). R2 had 54/90. - SQLite: 81/90 on both this head and
release/3.9.0, with identical per-operation results. - With B4 applied, PG17 is 81/90 and its failure set equals SQLite's.
- PG17: 80/90. The 10 failures are the 9 SQLite-baseline items plus
- CI
ubuntu-latestat 8526e90: failure.- Elsa.Persistence.Dapper.UnitTests: Failed 1, Passed 46, Skipped 0, Total 47 (R2's 43 plus 4 new). The PG test ran: it was reported at 9.7 s, and the assembly took 9 s.
- Overall: Failed 1, Passed 472, Skipped 2. The 2 skips are in other assemblies, as in R2.
- eec9581 failed the same way at publish.
B4: AndWorkflowInstanceSearchTerm now quotes Id (same spelling as definition search). The non-PG snapshot changes one token (ID → Id); SQLite and default-collation SQL Server are case-insensitive. B5: rec-pg-1 uses ActivityStatus.Completed, and the paged delete is built in the test so inner parameters are not dropped. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
|
Addressing Round 3 REQUEST_CHANGES B4/B5. There are no line-level review threads. Landed in dc596ed. B4 — instance search B5a — activity status. B5b — paged delete. Did not change Did not take N1 (SqlDialectBase template fold) or the optional SQLite DateTimeOffset handler change. |
The instance-search snapshot token is accepted. It also fixes instance search on case-sensitive SQL Server collations, where the column is Id. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
|
B4 wording correction (Crew Lead/CR): the |
sfmskywalker
left a comment
There was a problem hiding this comment.
Code Review, Round 4/4: APPROVE + HIGH @ a7a0bf6
Delta since Round 3 (8526e90e to a7a0bf60; commits 3a58d186, dc596edf, a7a0bf60): it contains exactly what Round 3 asked for, and nothing else.
-
B4 fixed, in the plain form.
AndWorkflowInstanceSearchTermnow usesquery.QuoteIdent("Id"), with no dialect sniff left anywhere insrc. This one line is the only non-test change in the delta. -
Snapshot. The only change in
NonPgQuerySqlSnapshotTestsis theinstance-searchtoken (ID→Id) plus a two-line comment, in the same commits as the code change. The PG shape assertions are added:"Id" like @SearchTermis present and"ID"is absent. -
B5 fixed.
- (a) The activity record now uses
Status = "Completed". - (b) The paged delete is built in the test with the inner parameters merged, and the
remainingassertion is kept. Store.DeleteAsyncis untouched, byte-identical torelease/3.9.0.
- (a) The activity record now uses
-
PR body and release note. Both carry:
- the accepted
ID→Idexception, noting that it also fixes instance search on case-sensitive SQL Server collations - the lowercase-schema rename caveat: an empty duplicate
"WorkflowDefinitions", the matchingVersionInforows, and recreate-and-re-import DELETE FROM "VersionInfo" WHERE "Version" = 10001;Fixes #255
The wording change asked for in Round 3 (hooks live in shared code, only PG overrides them) is in as well.
- the accepted
Verification (Release build, local PostgreSQL 17.11)
- Fresh-PG test. Passes when pointed at a fresh PG17 database; the only local change swaps in the connection string, since the box has no Docker.
- Elsa.Persistence.Dapper.UnitTests: 47/47. Elsa.Dapper.UnitTests: 16/16.
- Revert-proof. Putting
"ID"back fails 4 tests:- the fresh-PG test, with
42703: column "ID" does not exist - the PG shape assertion
- both the SQLite and SQL Server snapshots
- the fresh-PG test, with
- 88-operation store/runtime run (the Round 2 checklist, rebuilt; real Dapper stores plus IWorkflowHost/publisher end-to-end):
- PG17: 79/88. SQLite: 79/88. The per-operation results are identical.
- The 9 shared failures are the known pre-existing items, all out of scope:
CountDistinctAsync,Store.DeleteAsyncpaged, 3 bookmark-queue ops (#264), and 4 key-value ops (#263). - With the
Idfix reverted, PG drops to 78/88. The only extra failure isSummarizeManyAsync(SearchTerm)with 42703, so the run detects exactly this regression.
- CI at
a7a0bf60: green.ubuntu-latest,submit-nuget, GitGuardian and the CLA check all pass.- Elsa.Persistence.Dapper.UnitTests: Failed 0, Passed 47, Skipped 0, Total 47, in 10 s. The fresh-PG test ran against Testcontainers rather than being skipped: it is a plain
[Fact]that fails without Docker, and the assembly takes about 1 s locally without the container. - Overall: Passed 473, Skipped 2. The skips are the Slack and Azure Service Bus tests, the same as Round 3.
- Elsa.Persistence.Dapper.UnitTests: Failed 0, Passed 47, Skipped 0, Total 47, in 10 s. The fresh-PG test ran against Testcontainers rather than being skipped: it is a plain
Non-blocking
- Round 3's N1 (fold
PostgreSqlDialect's 14 re-implemented members intoSqlDialectBasetemplates viaQuoteIdentifier) and N3 (register the tolerantDateTimeOffsethandler from SQLite too) weren't taken up. That's fine; they're worth a 3.10 issue. - Merge interplay with #266:
- #266's R1 fix will touch the same line in
ParameterizedQueryBuilderExtensions.StartsWith(the key-valueStartsWithbinding). This PR changes that line to{query.QuoteIdent(field)} like @SearchTermLikebut leaves the binding as is. Whichever lands second has a small textual conflict; keep the quoting and the new parameter name together. - Both PRs also add package references to the Dapper test csproj, a trivial conflict; keep both.
- #266's R1 fix will touch the same line in
Fixes #255doesn't auto-close from a non-default base branch, so close #255 manually once #258 and #259 are both merged.
#258 quoted identifiers but still bound @{field} while emitting @SearchTermLike. Keep QuoteIdent and @{field}StartsWith together. Add PostgreSQL Testcontainers TryDelete coverage now that the infra is on the branch. Update the non-PG SQL snapshot for StartsWith. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
…sync (#266) * fix(dapper): create KeyValues and add BookmarkQueueItems.SerializedOptions Guarded V3.9 runtime migration (20008) so a migration-built DB can persist IKeyValueStore rows and bookmark-queue Options. Existing hand-created KeyValues tables and SerializedOptions columns are left alone. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> * fix(dapper): apply KV prefix or exact key, not both, and bind StartsWith Prefix FindMany threw on every provider because ApplyFilter combined Is(Id) with StartsWith, and StartsWith emitted @SearchTermLike while binding @{field}. Down() for 20008 is now a no-op so a rollback cannot drop a hand-created KeyValues table or SerializedOptions column. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> * chore(deps): pin ElsaVersion to 3.9.0-preview.5726 Final 3.9 cut pin. Core release/3.9.0 @ fa68369a includes #8539 (IKeyValueStore.TryDeleteAsync). ElsaStudioVersion stays 3.9.0-preview.1757. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> * fix(persistence): atomic TryDeleteAsync on Dapper and Mongo (#260) Override IKeyValueStore.TryDeleteAsync so legacy-pause adoption is a single count-checked delete. Dapper uses Store.DeleteAsync row count; Mongo uses DeleteOneAsync(ApplyTenantScope(...)).DeletedCount. Default-tenant reads and deletes now match NULL or '' TenantId so a legacy NULL row is visible to Tenant.Default (#245 gap for adoption). The rest of #245 stays on 3.10. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> * ci: retrigger pr workflow for #260 Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> * ci: touch TryDelete tests so pr.yml paths match Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> * fix(dapper): keep QuoteIdent with StartsWith binding after #258 rebase #258 quoted identifiers but still bound @{field} while emitting @SearchTermLike. Keep QuoteIdent and @{field}StartsWith together. Add PostgreSQL Testcontainers TryDelete coverage now that the infra is on the branch. Update the non-PG SQL snapshot for StartsWith. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> * test(dapper): fail if StartsWith drops QuoteIdent on PostgreSQL CR recommended: pin the quoted @NameStartsWith shape so taking this PR's unquoted StartsWith side fails a test, not only a local PG harness. Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Fixes #255
Cause
FluentMigrator 7.2
AddPostgres()registersPostgres15_0Processor. In 7.2.0 the postgres processors report:DatabaseTypeDatabaseTypeAliasesPostgresProcessorPostgresPostgreSQLPostgres10_0ProcessorPostgreSQL10_0PostgreSQL10_0,PostgreSQLPostgres11_0ProcessorPostgreSQL11_0PostgreSQL11_0,PostgreSQLPostgres15_0Processor(AddPostgres())PostgreSQL15_0PostgreSQL15_0,PostgreSQLPostgres92ProcessorPostgres92Postgres92,PostgreSQL92IfDatabasematching in 7.2:IfDatabase(params string[]): exactOrdinalIgnoreCaseagainstDatabaseTypeor aliases. This is the overload we use.IfDatabase(Predicate<string>): receives onlyDatabaseType. Do not use it here.A second blocker appears after migrate: FluentMigrator force-quotes PG identifiers (
"ActivityExecutionRecords"), while the Dapper store and shared query builder emitted unquoted names (42P01 / 42703).PostgreSqlDialect.Upsertomitted the PK (23502). VersionOptions compared boolean columns to integers ("IsPublished" = 0→ 42883). Instance search inlinedID, which quoted on PG is 42703 against"Id".After persist, Find failed because the SQLite string-only
DateTimeOffsethandler is registered onSqlMapperand Npgsql returnsDateTime.Fix
Migrations and the store fix stay together in this PR.
Migrations
Shared
MigrationDatabases.DateTimeOffsetProviderspassed to the alias-awareparamsoverload:SqlServer,Oracle,MySql,Postgres,PostgreSQL,PostgreSQL92Applied at
Management/Initial.cs(10001),Runtime/Initial.cs(20001), andRuntime/V3_3.cs(20004).Store (hooks in shared code; only PG overrides)
The hooks live in shared
ISqlDialect/ the query builder. OnlyPostgreSqlDialectoverrides them. Other providers' SQL is byte-identical to the pre-hook capture, except one token (see below).QuoteIdentifier(string name)— default returns the input unchanged.PostgreSqlDialectdouble-quotes.BooleanLiteral(bool value)— default emits1/0.PostgreSqlDialectemitstrue/false.Every identifier
ParameterizedQueryBuilderExtensionsinlines goes throughQuoteIdentifier.Is(VersionOptions)usesBooleanLiteral.TryMarkInterruptedAsyncquotesStatus.AndWorkflowInstanceSearchTermusesquery.QuoteIdent("Id")(same spelling as definition search; no dialect sniff).PostgreSqlDialectstill quotesFrom/And/Count/Delete/Upsert(PK included, statement terminated with;) and registers a PGDateTimeOffsethandler.Single intentional non-PG SQL change (release-lead sign-off)
AndWorkflowInstanceSearchTermused to emit unquotedID. It now emitsIdvia plainquery.QuoteIdent("Id"). The golden snapshot is updated in the same commit.This is the only exception to "byte-identical" non-PG SQL. It also fixes instance search on case-sensitive SQL Server collations, where the column is
Id.Handmade lowercase, unquoted PG schemas
Yes, quoted identifiers break those databases. That is accepted.
Compatibility position: the supported path is the FluentMigrator-created mixed-case quoted schema. Handmade lowercase unquoted schemas are unsupported. They never worked with the Dapper PG store: every upsert omitted the PK (23502) from 3.0 through 3.8.
Rename-path caveat: migrating against an un-renamed lowercase table creates an empty mixed-case duplicate (
"WorkflowDefinitions"next toworkflowdefinitions) because FluentMigrator'sTableExistsis case-sensitive. Renamed tables still need matchingVersionInforows, or later migrations fail. Otherwise, recreate via the migrations on an empty database and re-import any data. FluentMigrator quoting is not turned off.Release note
DELETE FROM "VersionInfo" WHERE "Version" = 10001;then migrate up. These databases cannot hold workflow data, so that one-line repair is enough.Idinstead ofID. That fixes instance search on case-sensitive SQL Server collations, where the column isId.Tests
AddSqlServer(),AddSQLite(),AddPostgres(),AddMySql(), andAddOracleManaged().ID→Idtoken.FindWorkflowGraphAsync(Published), VersionOptions Latest / Published / LatestOrPublished, Studio lists, journal and activity-execution reads, bookmark-queue paging,BeforeLastUpdated,TryMarkInterruptedAsync, and a paged delete built in the test so inner parameters are not dropped (Store.DeleteAsyncis unchanged; filed separately for 3.10).CI (
ubuntu-latest) has Docker. This agent VM does not.Main counterpart: #259