feat(persistence): executions + node_executions migrations and repositories
This commit is contained in:
@@ -74,10 +74,14 @@ builder.Services.AddSingleton<Migrator>(_ => new Migrator(new IMigration[]
|
||||
new Migration001Initial(),
|
||||
new Migration002Connections(),
|
||||
new Migration003Workflows(),
|
||||
new Migration004Executions(),
|
||||
new Migration005NodeExecutions(),
|
||||
}));
|
||||
builder.Services.AddSingleton<PasswordService>();
|
||||
builder.Services.AddSingleton<ConnectionRepository>();
|
||||
builder.Services.AddSingleton<WorkflowRepository>();
|
||||
builder.Services.AddSingleton<ExecutionRepository>();
|
||||
builder.Services.AddSingleton<NodeExecutionRepository>();
|
||||
builder.Services.AddSingleton<IConnectionSecretPolicy, SchemaSecretPolicy>();
|
||||
|
||||
builder.Services
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace VolcanicCore.Persistence.Migrations;
|
||||
|
||||
public sealed class Migration004Executions : IMigration
|
||||
{
|
||||
public int Version => 4;
|
||||
public void Apply(SqliteConnection conn)
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
CREATE TABLE executions (
|
||||
id TEXT PRIMARY KEY,
|
||||
workflow_id TEXT NOT NULL,
|
||||
trigger TEXT NOT NULL, -- 'nats.hook' | 'http.webhook' | 'manual'
|
||||
trigger_data_json TEXT NOT NULL,
|
||||
status TEXT NOT NULL, -- 'running' | 'completed' | 'partial' | 'failed' | 'aborted'
|
||||
started_at TEXT NOT NULL,
|
||||
ended_at TEXT,
|
||||
error TEXT
|
||||
);
|
||||
CREATE INDEX ix_executions_workflow_started ON executions (workflow_id, started_at DESC);
|
||||
CREATE INDEX ix_executions_ended_at ON executions (ended_at);
|
||||
";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace VolcanicCore.Persistence.Migrations;
|
||||
|
||||
public sealed class Migration005NodeExecutions : IMigration
|
||||
{
|
||||
public int Version => 5;
|
||||
public void Apply(SqliteConnection conn)
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
CREATE TABLE node_executions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
execution_id TEXT NOT NULL REFERENCES executions(id) ON DELETE CASCADE,
|
||||
node_id TEXT NOT NULL,
|
||||
inputs_json TEXT NOT NULL, -- [[items on port 0], [items on port 1], ...] or []
|
||||
outputs_json TEXT NOT NULL, -- [[items on out 0], ...] or []
|
||||
status TEXT NOT NULL, -- 'completed' | 'failed' | 'skipped_due_to_failure'
|
||||
started_at TEXT NOT NULL,
|
||||
ended_at TEXT NOT NULL,
|
||||
error TEXT
|
||||
);
|
||||
CREATE INDEX ix_node_executions_by_execution ON node_executions (execution_id);
|
||||
";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
121
src/VolcanicCore.Persistence/Repositories/ExecutionRepository.cs
Normal file
121
src/VolcanicCore.Persistence/Repositories/ExecutionRepository.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace VolcanicCore.Persistence.Repositories;
|
||||
|
||||
public sealed record ExecutionRow(
|
||||
string Id, string WorkflowId, string Trigger, string TriggerDataJson,
|
||||
string Status, DateTimeOffset StartedAt, DateTimeOffset? EndedAt, string? Error);
|
||||
|
||||
public sealed class ExecutionRepository
|
||||
{
|
||||
private readonly SqliteConnectionFactory _factory;
|
||||
public ExecutionRepository(SqliteConnectionFactory f) => _factory = f;
|
||||
|
||||
public string Create(string workflowId, string trigger, string triggerDataJson)
|
||||
{
|
||||
var id = Guid.NewGuid().ToString("N");
|
||||
var now = DateTimeOffset.UtcNow.ToString("o");
|
||||
using var conn = _factory.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
INSERT INTO executions (id, workflow_id, trigger, trigger_data_json, status, started_at)
|
||||
VALUES ($id, $w, $t, $td, 'running', $now);
|
||||
";
|
||||
cmd.Parameters.AddWithValue("$id", id);
|
||||
cmd.Parameters.AddWithValue("$w", workflowId);
|
||||
cmd.Parameters.AddWithValue("$t", trigger);
|
||||
cmd.Parameters.AddWithValue("$td", triggerDataJson);
|
||||
cmd.Parameters.AddWithValue("$now", now);
|
||||
cmd.ExecuteNonQuery();
|
||||
return id;
|
||||
}
|
||||
|
||||
public void SetStatus(string id, string status, string? error, DateTimeOffset? endedAt)
|
||||
{
|
||||
using var conn = _factory.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"UPDATE executions
|
||||
SET status = $s, error = $e, ended_at = $end
|
||||
WHERE id = $id;";
|
||||
cmd.Parameters.AddWithValue("$id", id);
|
||||
cmd.Parameters.AddWithValue("$s", status);
|
||||
cmd.Parameters.AddWithValue("$e", (object?)error ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("$end", endedAt is null ? DBNull.Value : (object)endedAt.Value.ToString("o"));
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public ExecutionRow? Get(string id)
|
||||
{
|
||||
using var conn = _factory.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"SELECT id, workflow_id, trigger, trigger_data_json, status,
|
||||
started_at, ended_at, error
|
||||
FROM executions WHERE id = $id;";
|
||||
cmd.Parameters.AddWithValue("$id", id);
|
||||
using var r = cmd.ExecuteReader();
|
||||
return r.Read() ? Materialize(r) : null;
|
||||
}
|
||||
|
||||
public IReadOnlyList<ExecutionRow> ListForWorkflow(string workflowId, int limit)
|
||||
{
|
||||
var rows = new List<ExecutionRow>();
|
||||
using var conn = _factory.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"SELECT id, workflow_id, trigger, trigger_data_json, status,
|
||||
started_at, ended_at, error
|
||||
FROM executions
|
||||
WHERE workflow_id = $w
|
||||
ORDER BY started_at DESC
|
||||
LIMIT $lim;";
|
||||
cmd.Parameters.AddWithValue("$w", workflowId);
|
||||
cmd.Parameters.AddWithValue("$lim", limit);
|
||||
using var r = cmd.ExecuteReader();
|
||||
while (r.Read()) rows.Add(Materialize(r));
|
||||
return rows;
|
||||
}
|
||||
|
||||
public int AbortAllRunning()
|
||||
{
|
||||
using var conn = _factory.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"UPDATE executions
|
||||
SET status = 'aborted', ended_at = $now
|
||||
WHERE status = 'running';";
|
||||
cmd.Parameters.AddWithValue("$now", DateTimeOffset.UtcNow.ToString("o"));
|
||||
return cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public int PruneOlderThan(DateTimeOffset cutoff)
|
||||
{
|
||||
using var conn = _factory.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"DELETE FROM executions
|
||||
WHERE ended_at IS NOT NULL AND ended_at < $c;";
|
||||
cmd.Parameters.AddWithValue("$c", cutoff.ToString("o"));
|
||||
return cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public int PruneOldestPerWorkflow(int keep)
|
||||
{
|
||||
// For each workflow, delete rows that are not in the newest `keep` by started_at.
|
||||
using var conn = _factory.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
DELETE FROM executions WHERE id IN (
|
||||
SELECT id FROM (
|
||||
SELECT id, ROW_NUMBER() OVER (
|
||||
PARTITION BY workflow_id ORDER BY started_at DESC
|
||||
) AS rn
|
||||
FROM executions
|
||||
) WHERE rn > $keep
|
||||
);";
|
||||
cmd.Parameters.AddWithValue("$keep", keep);
|
||||
return cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static ExecutionRow Materialize(SqliteDataReader r) => new(
|
||||
r.GetString(0), r.GetString(1), r.GetString(2), r.GetString(3), r.GetString(4),
|
||||
DateTimeOffset.Parse(r.GetString(5)),
|
||||
r.IsDBNull(6) ? null : DateTimeOffset.Parse(r.GetString(6)),
|
||||
r.IsDBNull(7) ? null : r.GetString(7));
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace VolcanicCore.Persistence.Repositories;
|
||||
|
||||
public sealed record NodeExecutionRow(
|
||||
long Id, string ExecutionId, string NodeId,
|
||||
string InputsJson, string OutputsJson,
|
||||
string Status, DateTimeOffset StartedAt, DateTimeOffset EndedAt, string? Error);
|
||||
|
||||
public sealed class NodeExecutionRepository
|
||||
{
|
||||
private readonly SqliteConnectionFactory _factory;
|
||||
public NodeExecutionRepository(SqliteConnectionFactory f) => _factory = f;
|
||||
|
||||
public void Create(string executionId, string nodeId,
|
||||
string inputsJson, string outputsJson,
|
||||
string status, DateTimeOffset startedAt, DateTimeOffset endedAt, string? error)
|
||||
{
|
||||
using var conn = _factory.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
INSERT INTO node_executions
|
||||
(execution_id, node_id, inputs_json, outputs_json, status, started_at, ended_at, error)
|
||||
VALUES ($eid, $nid, $inp, $out, $s, $sa, $ea, $err);
|
||||
";
|
||||
cmd.Parameters.AddWithValue("$eid", executionId);
|
||||
cmd.Parameters.AddWithValue("$nid", nodeId);
|
||||
cmd.Parameters.AddWithValue("$inp", inputsJson);
|
||||
cmd.Parameters.AddWithValue("$out", outputsJson);
|
||||
cmd.Parameters.AddWithValue("$s", status);
|
||||
cmd.Parameters.AddWithValue("$sa", startedAt.ToString("o"));
|
||||
cmd.Parameters.AddWithValue("$ea", endedAt.ToString("o"));
|
||||
cmd.Parameters.AddWithValue("$err", (object?)error ?? DBNull.Value);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public IReadOnlyList<NodeExecutionRow> ListByExecution(string executionId)
|
||||
{
|
||||
var rows = new List<NodeExecutionRow>();
|
||||
using var conn = _factory.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"SELECT id, execution_id, node_id, inputs_json, outputs_json,
|
||||
status, started_at, ended_at, error
|
||||
FROM node_executions
|
||||
WHERE execution_id = $eid
|
||||
ORDER BY started_at ASC, id ASC;";
|
||||
cmd.Parameters.AddWithValue("$eid", executionId);
|
||||
using var r = cmd.ExecuteReader();
|
||||
while (r.Read())
|
||||
rows.Add(new NodeExecutionRow(
|
||||
r.GetInt64(0), r.GetString(1), r.GetString(2), r.GetString(3), r.GetString(4),
|
||||
r.GetString(5),
|
||||
DateTimeOffset.Parse(r.GetString(6)),
|
||||
DateTimeOffset.Parse(r.GetString(7)),
|
||||
r.IsDBNull(8) ? null : r.GetString(8)));
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user