fix(graph): migration 002 now tolerates legacy graph_nodes/edges schemas

Rewrites migration 002 to use ALTER TABLE ... IF EXISTS with per-column guards
and a data-migration DO block that backfills document_json/written_at/batch_id
from the older (tenant_id, data, created_at) layout when present. Updates
GraphChangeStreamProcessor + SavedViewsMigrationHostedService for the aligned
schema and extends the incremental processor tests for the new path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-13 21:57:54 +03:00
parent 257e29355b
commit d613f8f2a4
4 changed files with 172 additions and 36 deletions

View File

@@ -3,6 +3,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Graph.Indexer.Ingestion.Sbom;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text.Json.Nodes;
@@ -37,30 +38,30 @@ public sealed class GraphChangeStreamProcessor : BackgroundService
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
protected override Task ExecuteAsync(CancellationToken stoppingToken)
=> RunPollingLoopAsync(stoppingToken);
internal async Task RunPollingLoopAsync(CancellationToken stoppingToken)
{
using var pollTimer = new PeriodicTimer(_options.PollInterval);
using var backfillTimer = new PeriodicTimer(_options.BackfillInterval);
var backfillStopwatch = Stopwatch.StartNew();
while (!stoppingToken.IsCancellationRequested)
try
{
var pollTask = pollTimer.WaitForNextTickAsync(stoppingToken).AsTask();
var backfillTask = backfillTimer.WaitForNextTickAsync(stoppingToken).AsTask();
var completed = await Task.WhenAny(pollTask, backfillTask).ConfigureAwait(false);
if (completed.IsCanceled || stoppingToken.IsCancellationRequested)
{
break;
}
if (completed == pollTask)
while (await pollTimer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false))
{
await ApplyStreamAsync(isBackfill: false, stoppingToken).ConfigureAwait(false);
if (_options.BackfillInterval <= TimeSpan.Zero || backfillStopwatch.Elapsed >= _options.BackfillInterval)
{
await ApplyStreamAsync(isBackfill: true, stoppingToken).ConfigureAwait(false);
backfillStopwatch.Restart();
}
}
else
{
await ApplyStreamAsync(isBackfill: true, stoppingToken).ConfigureAwait(false);
}
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// Graceful BackgroundService shutdown.
}
}