Skip to content

Fix ActivityNodeId column truncation for deeply nested workflows#7338

Merged
sfmskywalker merged 5 commits intorelease/3.6.0from
copilot/fix-truncation-error-nested-workflows
Mar 2, 2026
Merged

Fix ActivityNodeId column truncation for deeply nested workflows#7338
sfmskywalker merged 5 commits intorelease/3.6.0from
copilot/fix-truncation-error-nested-workflows

Conversation

Copy link
Contributor

Copilot AI commented Mar 2, 2026

Deeply nested workflows produce ActivityNodeId values that exceed fixed column sizes (nvarchar(450) on SQL Server, varchar(255) on MySQL, NVARCHAR2(450) on Oracle), causing String or binary data would be truncated errors on insert.

Column type changes

Widen ActivityNodeId to the provider-native unlimited type in V3_6 migrations:

Provider Before After
SQL Server nvarchar(450) nvarchar(max)
MySQL varchar(255) longtext
Oracle NVARCHAR2(450) NCLOB
PostgreSQL text no change
SQLite TEXT no change

Index removal

  • Drop IX_ActivityExecutionRecord_ActivityNodeId and IX_WorkflowExecutionLogRecord_ActivityNodeId — unlimited-length columns cannot be B-tree indexed
  • Remove corresponding HasIndex calls from Configurations.cs
  • No performance impact: every query on ActivityNodeId also filters by WorkflowInstanceId, which has its own index

Defensive migration DDL

DropIndex calls use provider-specific existence guards to avoid failures on partial re-runs:

// SQL Server — matches existing pattern in the same migration
migrationBuilder.Sql($@"
    IF EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_ActivityExecutionRecord_ActivityNodeId'
        AND object_id = OBJECT_ID('{_schema.Schema}.ActivityExecutionRecords'))
    BEGIN
        DROP INDEX [IX_ActivityExecutionRecord_ActivityNodeId]
        ON [{_schema.Schema}].[ActivityExecutionRecords]
    END
");
  • PostgreSQL / SQLite: DROP INDEX IF EXISTS (native syntax)
  • MySQL: information_schema.statistics check with PREPARE/EXECUTE
  • Oracle: PL/SQL block catching ORA-01418 (index does not exist)
Original prompt

This section details on the original issue you should resolve

<issue_title>Unable to store long ActivityNodeId values < NVARCHAR(450); value truncated on SQL Server</issue_title>
<issue_description>## Description
When using SQL Server as a persistence provider for Elsa-Core, attempting to run deeply nested workflows (or using activities with long node IDs) can result in an error indicating that a value is too large to insert into the database, even though the node ID is less than the configured NVARCHAR(450) column size. The problem manifests as a truncation error with activity node IDs that are long but appear to be within the specified column limits.

  • Example value that triggers the error: Workflow11:lawgatg1n7qqjrzk:n5w6jfti43uwrc9i:Flowchart2:sdh8wlmjumbebayi:Workflow10:okmk2fhbtpc40qe0
Code sample of model configuration for ActivityNodeId

Steps to Reproduce

  1. Use SQL Server as the DB provider.
  2. Create a series of nested workflows (or assign a node ID with ~100 characters).
  3. Ensure the workflow/activity depth is at least 4.
  4. Node IDs should cumulatively have a substantial length, but remain under the configured NVARCHAR(450).
  5. Run the workflow and observe attempt to persist execution data.

Expected Behavior

Values under 450 characters for ActivityNodeId should be stored without error.

Actual Behavior

Error: String or binary data would be truncated in table ...Elsa.ActivityExecutionRecords, column 'ActivityNodeId'. Truncated value: 'Workflow11:lawgatg1n7qqjrzk:n5w6jfti43uwrc9i:Flowchart2:sdh8wlmjumbebayi:Workflow10:okmk2fhbtpc40qe0'.

Similar error occurs for Elsa.WorkflowExecutionLogRecords, column 'ActivityNodeId'.

Screenshots

See model configuration snippet (above).

Environment

  • Elsa Package Version: [specify version]
  • Operating System: [specify]
  • Browser and Version: N/A

Log Output

See detailed stack trace in the issue body (MassTransit and EF Core errors, SQL exception 2628).

Troubleshooting Attempts

The DB column appears to already be set to NVARCHAR(450). Investigate if the framework or related code is transforming/hashing the node ID in a way that increases its length past 450 characters before inserting to DB.

Additional Context

  • Happens on at least two tables: ActivityExecutionRecords and WorkflowExecutionLogRecords.
  • It may not be a direct model configuration issue, but possibly with how the value is processed before storage (e.g., hashing?).
  • Reproducible with moderately large node identifiers and moderate nesting.

Related Issues

Let's check all tables/entities where NodeId is stored for similar configuration or transformation logic.</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 2 commits March 2, 2026 12:47
…ncation for deeply nested workflows

Add HasMaxLength(1024) for ActivityNodeId on ActivityExecutionRecord and
WorkflowExecutionLogRecord entities. Create V3_7 migrations for all 5
database providers (SqlServer, MySql, PostgreSql, Sqlite, Oracle).

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
…odeId

Update V3_6 migrations to change ActivityNodeId from fixed-length columns
to unlimited types: nvarchar(max) for SQL Server, longtext for MySQL,
NCLOB for Oracle. PostgreSQL (text) and SQLite (TEXT) already used
unlimited types. Drop ActivityNodeId indexes on both ActivityExecutionRecords
and WorkflowExecutionLogRecords tables since unlimited columns cannot be
indexed. Remove HasIndex for ActivityNodeId from Configurations.cs.

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix truncation error for long ActivityNodeId in SQL Server Fix ActivityNodeId column truncation for deeply nested workflows Mar 2, 2026
@sfmskywalker sfmskywalker marked this pull request as ready for review March 2, 2026 13:26
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 2, 2026

Greptile Summary

This PR fixes a truncation error on ActivityNodeId columns in ActivityExecutionRecords and WorkflowExecutionLogRecords by widening the column type to the provider-native unlimited text type (nvarchar(max), longtext, NCLOB) for SQL Server, MySQL, and Oracle, and by dropping the associated B-tree index (which cannot span an unlimited-length column) for all five providers including PostgreSQL and SQLite. The Configurations.cs EF Core model configuration is updated to match.

Key concerns found during review:

  • Mutating existing migrations instead of adding a new one: All five provider V3_6 migration files are modified in-place. Because EF Core tracks applied migrations by file name in __EFMigrationsHistory, any database that has already run V3_6 will not receive these schema changes automatically. A new incremental migration (e.g. V3_6_1) would be the standard, safe approach.
  • Missing IF EXISTS guards on DropIndex: The SQL Server migration already uses a conditional IF EXISTS SQL block to drop the Trigger index safely, but the two new DropIndex calls for ActivityNodeId use the plain EF Core DropIndex API with no existence check. If those indexes are absent for any reason (manual DDL, partial previous run, etc.), the migration will throw and abort. The same applies to all five provider files.
  • Down() migration risks data loss: For MySQL (longtextvarchar(255)), Oracle (NCLOBNVARCHAR2(450)), and SQL Server (nvarchar(max)nvarchar(450)), rolling back will silently truncate or error on rows with ActivityNodeId values that exceed the original column size — the very data this PR exists to support.

Confidence Score: 2/5

  • Not safe to merge without addressing the in-place migration mutation and missing existence guards — risks leaving already-deployed databases in the broken state the PR intends to fix.
  • The column widening and index removal are the correct fix, and the model snapshots are consistent. However, modifying existing V3_6 migration files means environments that already applied V3_6 will never receive the fix. Additionally, missing IF EXISTS guards on all five providers can cause migration failures on edge-case databases, and the Down() path can silently truncate data that was only possible to write because of the Up() change.
  • All five _V3_6.cs migration files (SqlServer, MySql, Oracle, PostgreSql, Sqlite) need the DropIndex existence guards and ideally should be replaced by a new incremental migration.

Important Files Changed

Filename Overview
src/modules/Elsa.Persistence.EFCore/Modules/Runtime/Configurations.cs Removes HasIndex on ActivityNodeId for both ActivityExecutionRecord and WorkflowExecutionLogRecord — correctly reflects that unlimited-length columns cannot carry a B-tree index.
src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/20251204150326_V3_6.cs Widens ActivityNodeId to nvarchar(max) and drops its index; however the DropIndex calls have no IF EXISTS guard (unlike the existing Trigger-index pattern in the same file), and the Down() migration narrows back to nvarchar(450) which will truncate or fail for long values. The migration also mutates the existing V3_6 file instead of being a new incremental migration.
src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/20251204150235_V3_6.cs Widens ActivityNodeId to longtext and drops its index; Down() narrows back to varchar(255) risking silent truncation of long values, and DropIndex calls have no existence guard.
src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/20251204150355_V3_6.cs Widens ActivityNodeId to NCLOB and drops its index; Down() narrows back to NVARCHAR2(450) risking data truncation, and DropIndex calls have no existence guard.
src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/20251204150341_V3_6.cs PostgreSQL column is already text (unbounded), so only the index is dropped; Down() recreates the index correctly. DropIndex still has no IF EXISTS guard.
src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/20251204150006_V3_6.cs SQLite column is already TEXT (unbounded), so only the index is dropped; Down() recreates the index correctly. DropIndex still has no IF EXISTS guard.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[EF Core Migration: V3_6 Up] --> B{Provider?}
    B --> |SQL Server| C[DropIndex ActivityNodeId\nnvarchar 450 → nvarchar max]
    B --> |MySQL| D[DropIndex ActivityNodeId\nvarchar 255 → longtext]
    B --> |Oracle| E[DropIndex ActivityNodeId\nNVARCHAR2 450 → NCLOB]
    B --> |PostgreSQL| F[DropIndex ActivityNodeId\ncolumn already text - no AlterColumn]
    B --> |SQLite| G[DropIndex ActivityNodeId\ncolumn already TEXT - no AlterColumn]
    C --> H[ActivityExecutionRecords\nActivityNodeId: nvarchar max]
    D --> I[ActivityExecutionRecords\nActivityNodeId: longtext]
    E --> J[ActivityExecutionRecords\nActivityNodeId: NCLOB]
    F --> K[Index removed only]
    G --> K

    H --> L[WorkflowExecutionLogRecords\nActivityNodeId: nvarchar max]
    I --> M[WorkflowExecutionLogRecords\nActivityNodeId: longtext]
    J --> N[WorkflowExecutionLogRecords\nActivityNodeId: NCLOB]

    L --> O[No index on ActivityNodeId]
    M --> O
    N --> O
    K --> O

    style C fill:#f9f,stroke:#333
    style D fill:#f9f,stroke:#333
    style E fill:#f9f,stroke:#333
    style F fill:#bbf,stroke:#333
    style G fill:#bbf,stroke:#333
Loading

Last reviewed commit: 6c73d5d

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

16 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +48 to +56
migrationBuilder.DropIndex(
name: "IX_WorkflowExecutionLogRecord_ActivityNodeId",
schema: _schema.Schema,
table: "WorkflowExecutionLogRecords");

migrationBuilder.DropIndex(
name: "IX_ActivityExecutionRecord_ActivityNodeId",
schema: _schema.Schema,
table: "ActivityExecutionRecords");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DropIndex calls lack an existence guard

The SQL Server Up() method already uses an IF EXISTS guard when dropping the Trigger index (lines 22–29). The two new DropIndex calls for ActivityNodeId do not apply the same pattern. Because this PR mutates an existing migration (rather than adding a new one), any database where those indexes were already manually dropped — or where a partial migration was applied — will throw a "Cannot drop the index … because it does not exist" error and abort the migration entirely.

The same issue exists in all five provider migration files (Oracle, MySQL, PostgreSQL, SQLite).

For SQL Server the safest fix mirrors the existing pattern already in this file:

migrationBuilder.Sql($@"
    IF EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_WorkflowExecutionLogRecord_ActivityNodeId'
        AND object_id = OBJECT_ID('{_schema.Schema}.WorkflowExecutionLogRecords'))
    BEGIN
        DROP INDEX [IX_WorkflowExecutionLogRecord_ActivityNodeId]
        ON [{_schema.Schema}].[WorkflowExecutionLogRecords]
    END
");

migrationBuilder.Sql($@"
    IF EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_ActivityExecutionRecord_ActivityNodeId'
        AND object_id = OBJECT_ID('{_schema.Schema}.ActivityExecutionRecords'))
    BEGIN
        DROP INDEX [IX_ActivityExecutionRecord_ActivityNodeId]
        ON [{_schema.Schema}].[ActivityExecutionRecords]
    END
");

For MySQL, PostgreSQL, Oracle, and SQLite equivalent IF EXISTS SQL clauses (or provider-specific conditional DDL) should be applied.

Comment on lines +91 to +123
migrationBuilder.AlterColumn<string>(
name: "ActivityNodeId",
schema: _schema.Schema,
table: "WorkflowExecutionLogRecords",
type: "varchar(255)",
nullable: false,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");

migrationBuilder.AlterColumn<string>(
name: "ActivityNodeId",
schema: _schema.Schema,
table: "ActivityExecutionRecords",
type: "varchar(255)",
nullable: false,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");

migrationBuilder.CreateIndex(
name: "IX_WorkflowExecutionLogRecord_ActivityNodeId",
schema: _schema.Schema,
table: "WorkflowExecutionLogRecords",
column: "ActivityNodeId");

migrationBuilder.CreateIndex(
name: "IX_ActivityExecutionRecord_ActivityNodeId",
schema: _schema.Schema,
table: "ActivityExecutionRecords",
column: "ActivityNodeId");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Down migration will silently truncate or fail on existing long-value rows

The MySQL Down() migration narrows ActivityNodeId back from longtext to varchar(255). Any row that was inserted with a value longer than 255 characters (the exact scenario this PR was created to fix) will either be silently truncated (when sql_mode does not include STRICT_TRANS_TABLES) or cause the rollback to fail entirely (when strict mode is on).

The same risk applies to the Oracle Down() migration (NCLOBNVARCHAR2(450)) and the SQL Server Down() migration (nvarchar(max)nvarchar(450)).

Because data loss on rollback is irreversible, the Down() migration should at minimum document this limitation with a comment, or guard the column change with an explicit truncation/assertion step that makes the data-loss risk visible rather than silent.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Please look into this comment.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 2, 2026

Additional Comments (1)

src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/20251204150326_V3_6.cs
Mutating an existing migration is risky for already-deployed databases

This PR amends the V3_6 migration files in-place rather than introducing a new incremental migration (e.g. V3_6_1). EF Core tracks applied migrations by name in the __EFMigrationsHistory table. Any environment that has already run V3_6 — including CI databases, staging, or early-adopters on the release/3.6.0 branch — will have the migration marked as applied and EF Core will not re-run it. Those databases will still have the narrow nvarchar(450) column and the old index, and will continue to experience the truncation error.

The safer approach is to introduce a new migration file (e.g. 20260302_V3_6_1.cs) that only contains the DropIndex + AlterColumn operations, leaving the original V3_6 untouched. This applies to all five provider migration directories.

@sfmskywalker
Copy link
Member

@copilot Take a look at the comments on this PR (from @graptile-apps, specifically). However, ignore its comments about existing DBs - we're working on unreleased code, so it's OK to have been updating existing migrations.

What I am mostly interested in, is: are we using any (DB) queries on NodeId anywhere where the DB index would have been important in terms of perf? Can you check?

Copy link
Contributor Author

Copilot AI commented Mar 2, 2026

@copilot Take a look at the comments on this PR (from @graptile-apps, specifically). However, ignore its comments about existing DBs - we're working on unreleased code, so it's OK to h...

I checked every place in the codebase where ActivityNodeId / ActivityNodeIds is used as a query filter:

  1. ActivityExecutions/List/Endpoint.cs — filters by WorkflowInstanceId + ActivityNodeId
  2. ActivityExecutionSummaries/ListSummaries/Endpoint.cs — filters by WorkflowInstanceId + ActivityNodeId
  3. ActivityExecutionStatsService.GetStatsAsync — filters by WorkflowInstanceId + ActivityNodeIds
  4. Journal/FilteredList/Endpoint.cs — filters by WorkflowInstanceId + optionally ActivityNodeIds
  5. WorkflowInstanceFinder.cs (Alterations) — sets ActivityNodeId but results are intersected with a separate WorkflowInstanceFilter query

In every case, WorkflowInstanceId is always present alongside ActivityNodeId. Since WorkflowInstanceId already has its own index (IX_ActivityExecutionRecord_WorkflowInstanceId / IX_WorkflowExecutionLogRecord_WorkflowInstanceId), the DB engine will use that index to narrow results first, then filter by ActivityNodeId in the reduced set. The standalone ActivityNodeId index was never being used as the primary lookup path, so dropping it has no meaningful performance impact.

Copilot AI and others added 2 commits March 2, 2026 14:08
…oviders

Replace bare migrationBuilder.DropIndex() calls with provider-specific
conditional DDL to prevent errors when indexes have already been dropped:
- SQL Server: IF EXISTS (SELECT * FROM sys.indexes ...) pattern
- PostgreSQL: DROP INDEX IF EXISTS (native support)
- SQLite: DROP INDEX IF EXISTS (native support)
- MySQL: information_schema.statistics check with PREPARE/EXECUTE
- Oracle: PL/SQL BEGIN...EXCEPTION WHEN OTHERS (SQLCODE -1418)

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
@sfmskywalker sfmskywalker merged commit 7143859 into release/3.6.0 Mar 2, 2026
2 checks passed
@sfmskywalker sfmskywalker deleted the copilot/fix-truncation-error-nested-workflows branch March 2, 2026 14:24
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.

Unable to store long ActivityNodeId values < NVARCHAR(450); value truncated on SQL Server

3 participants