Skip to content

Implement SQLite workflow persistence - #6

Merged
rezaqomy merged 1 commit into
masterfrom
agent/sqlite-workflow-persistence
Aug 8, 2026
Merged

Implement SQLite workflow persistence#6
rezaqomy merged 1 commit into
masterfrom
agent/sqlite-workflow-persistence

Conversation

@rezaqomy

@rezaqomy rezaqomy commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Implementation summary

  • Added a SQLite-backed workflow store under internal/store/sqlite.
  • Added database initialization and an idempotent migration runner.
  • Stores active workflow heads separately from immutable workflow version rows.
  • Supports create, read latest, read specific version, list active workflows, update-as-new-version, and delete.
  • Wired the server to persist workflows in --data-dir/flowforge.db.
  • Updated workflow store callers to pass context.Context.
  • Updated documentation for SQLite workflow persistence.

Architecture decisions

  • Kept SQLite-specific imports inside internal/store/sqlite; kernel packages do not import SQLite.
  • Preserved the generic store.WorkflowStore boundary and added GetVersion plus WorkflowVersion as storage-level concepts.
  • Updates append version N+1 in the same transaction that advances the workflow head.
  • Deletes mark the workflow head as deleted while leaving historical versions readable.
  • Stored canonical JSON for workflow resources and metadata to avoid YAML parser coupling in the persistence layer.

Tests executed

  • GOCACHE=/tmp/flowforge-go-build go test ./...
  • GOCACHE=/tmp/flowforge-go-build go vet ./...

Known limitations

  • SQLite workflow persistence uses github.com/mattn/go-sqlite3, so builds require CGO support.
  • No HTTP endpoint or CLI command exposes historical workflow versions yet.
  • Existing file-backed workflow store remains available for tests/compatibility but is not a full historical version backend.

Migration notes

  • New server workflow state is stored at --data-dir/flowforge.db.
  • Existing JSON workflow files under --data-dir/workflows are not migrated automatically.
  • Run persistence, execution checkpoints, triggers, and UI remain out of scope.

Summary by CodeRabbit

  • New Features

    • Workflows now use SQLite database for persistence under the data directory
    • Immutable workflow versions automatically tracked with each update
    • Secrets encrypted and stored as files in the data directory
  • Documentation

    • Updated architecture and development documentation regarding workflow persistence and storage details

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces a SQLite-backed WorkflowStore with immutable versioned workflow storage and schema migrations. The WorkflowStore interface and all implementations (MemoryWorkflowStore, FileWorkflowStore) are updated to be context-aware with a new WorkflowVersion type. API handlers and the server entry point are wired to pass contexts and use the new SQLite backend.

Changes

SQLite Workflow Persistence

Layer / File(s) Summary
WorkflowStore interface and WorkflowVersion type
internal/store/workflow_store.go, go.mod
Adds WorkflowVersion struct (name, version, resource, metadata, CreatedAt) and updates all WorkflowStore method signatures to accept context.Context; adds github.com/mattn/go-sqlite3 v1.14.46 dependency.
SQLite WorkflowStore implementation
internal/store/sqlite/doc.go, internal/store/sqlite/workflow_store.go
OpenWorkflowStore opens a SQLite DB with busy-timeout and foreign-key pragmas, runs Migrate; Migrate applies versioned schema SQL transactionally via schema_migrations; writeVersion enforces create/update mode invariants, upserts head rows, and inserts immutable workflow_versions rows; Get/GetVersion/List/Delete implement reads and soft-deletion with JSON decoding and name validation.
SQLite WorkflowStore tests
internal/store/sqlite/workflow_store_test.go
Tests full CRUD lifecycle with version assertions, persistence across store restart, migration idempotency, and ErrWorkflowNotFound on update-missing.
Memory and file store interface conformance
internal/store/memory_workflow_store.go, internal/store/file_workflow_store.go, internal/store/memory_workflow_store_test.go, internal/store/file_workflow_store_test.go
MemoryWorkflowStore gains a versions map, appendVersionLocked helper, and GetVersion; FileWorkflowStore gains a GetVersion stub returning time.Time{}; both stores' method signatures updated to accept context parameters; tests updated to pass context.Background().
API context threading and server wiring
internal/api/workflows.go, internal/api/telegram_webhook.go, cmd/server/main.go
All HTTP workflow handlers pass r.Context() to store calls; server switches from FileWorkflowStore to sqlite.OpenWorkflowStore with deferred Close and context-threaded startup registration and event dispatch.
Documentation updates
README.md, docs/architecture.md, docs/development.md
Replaces "reserved for future use" notes with descriptions of the implemented SQLite persistence layer, immutable versioning semantics, and store interface/implementation boundaries.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • rezaqomy/FlowForge#5: Both PRs modify internal/api/telegram_webhook.go workflow listing — the earlier PR introduced the Telegram webhook handler that calls List, which this PR updates to pass r.Context().

Poem

🐰 Hoppity-hop, I dug in the ground,
And a SQLite burrow is what I have found!
Each workflow version, immutable and neat,
Context flows through every receipt.
No more files scattered about—
The database bunny has sorted it out! 🗄️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Implement SQLite workflow persistence' accurately and clearly describes the main change in the pull request—the introduction of SQLite-backed workflow persistence to replace file-based storage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/sqlite-workflow-persistence

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
internal/store/file_workflow_store_test.go (1)

9-45: ⚡ Quick win

Please add explicit GetVersion assertions here.

This lifecycle test now exercises context plumbing, but not the newly added GetVersion API. Add an assertion for the intended file-store behavior so interface conformance can’t silently regress.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/store/file_workflow_store_test.go` around lines 9 - 45, The
TestFileWorkflowStorePersistsLifecycle test exercises the Create, Update, and
Delete operations but does not test the GetVersion API, which means interface
conformance changes could regress silently. Add explicit assertions for the
GetVersion API by calling it at key points in the test lifecycle (after the
initial Create operation and after the Update operation) and verify that the
returned version values are as expected, ensuring the version tracking behavior
is properly validated alongside the other store operations.
cmd/server/main.go (1)

63-63: ⚡ Quick win

Capture and log Close() errors during shutdown.

defer workflowStore.Close() drops any close error, which can hide SQLite finalization failures.

Suggested change
-	defer workflowStore.Close()
+	defer func() {
+		if err := workflowStore.Close(); err != nil {
+			log.Printf("close workflow store: %v", err)
+		}
+	}()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/server/main.go` at line 63, The defer statement for workflowStore.Close()
currently ignores any error returned by the Close method, which can hide SQLite
finalization failures. Modify the defer statement to capture the error returned
by workflowStore.Close() and log it using an appropriate logging mechanism if an
error occurs. This ensures that any close-time failures are visible in the logs
rather than being silently dropped.
internal/store/memory_workflow_store_test.go (1)

11-47: ⚡ Quick win

Add direct coverage for the new versioning contract.

These tests validate CRUD paths, but not GetVersion or version increments introduced in the memory store. Please add assertions for version 1 after create, version 2 after update, and expected behavior after delete.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/store/memory_workflow_store_test.go` around lines 11 - 47, The tests
TestMemoryWorkflowStoreLifecycle and TestMemoryWorkflowStoreUpdateMissing
validate CRUD operations but do not assert on the versioning contract. Add
assertions in TestMemoryWorkflowStoreLifecycle to verify that GetVersion returns
1 after the initial Create call, returns 2 after the Update call, and verify the
expected behavior when calling GetVersion after the Delete call. If versioning
applies to the UpdateMissing scenario, add similar version assertions to
TestMemoryWorkflowStoreUpdateMissing as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/store/file_workflow_store.go`:
- Around line 45-59: The GetVersion method in FileWorkflowStore incorrectly
returns the current workflow content while claiming it is version 1, which
violates the versioning contract. Since this file-based store cannot maintain
historical versions (it only stores the latest state), modify the method to
return a consistent error (such as ErrWorkflowNotFound) for all version requests
instead of fabricating version data. Remove the logic that reads the current
workflow and returns it as version 1, and ensure the method either accepts only
a specific version number that indicates "current" or rejects all version
requests with an appropriate error that reflects the store's inability to
support versioning.

In `@internal/store/memory_workflow_store.go`:
- Around line 100-109: In the appendVersionLocked method of MemoryWorkflowStore,
the Workflow field is storing a shallow copy of the workflow parameter, which
allows mutations to reference types (maps/slices) within kernel.WorkflowResource
to retroactively modify historical versions. Create a deep copy of the workflow
parameter before storing it in the WorkflowVersion struct to ensure version
history immutability. This deep copy should be created before the append
operation and assigned to the Workflow field in the WorkflowVersion
initialization.

In `@internal/store/sqlite/workflow_store.go`:
- Around line 41-43: The db.Close() call on line 42 in the migration error
handling path is not checking for errors, which violates error handling best
practices and triggers errcheck warnings. Modify the error handling to capture
the error returned by db.Close() when workflowStore.Migrate fails, and either
log the close error or wrap it together with the original migration error before
returning, ensuring cleanup failures are not silently ignored.

---

Nitpick comments:
In `@cmd/server/main.go`:
- Line 63: The defer statement for workflowStore.Close() currently ignores any
error returned by the Close method, which can hide SQLite finalization failures.
Modify the defer statement to capture the error returned by
workflowStore.Close() and log it using an appropriate logging mechanism if an
error occurs. This ensures that any close-time failures are visible in the logs
rather than being silently dropped.

In `@internal/store/file_workflow_store_test.go`:
- Around line 9-45: The TestFileWorkflowStorePersistsLifecycle test exercises
the Create, Update, and Delete operations but does not test the GetVersion API,
which means interface conformance changes could regress silently. Add explicit
assertions for the GetVersion API by calling it at key points in the test
lifecycle (after the initial Create operation and after the Update operation)
and verify that the returned version values are as expected, ensuring the
version tracking behavior is properly validated alongside the other store
operations.

In `@internal/store/memory_workflow_store_test.go`:
- Around line 11-47: The tests TestMemoryWorkflowStoreLifecycle and
TestMemoryWorkflowStoreUpdateMissing validate CRUD operations but do not assert
on the versioning contract. Add assertions in TestMemoryWorkflowStoreLifecycle
to verify that GetVersion returns 1 after the initial Create call, returns 2
after the Update call, and verify the expected behavior when calling GetVersion
after the Delete call. If versioning applies to the UpdateMissing scenario, add
similar version assertions to TestMemoryWorkflowStoreUpdateMissing as well.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c5bebd80-d69e-431d-9395-246e6b070430

📥 Commits

Reviewing files that changed from the base of the PR and between 9a7eb78 and 4330ee4.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (15)
  • README.md
  • cmd/server/main.go
  • docs/architecture.md
  • docs/development.md
  • go.mod
  • internal/api/telegram_webhook.go
  • internal/api/workflows.go
  • internal/store/file_workflow_store.go
  • internal/store/file_workflow_store_test.go
  • internal/store/memory_workflow_store.go
  • internal/store/memory_workflow_store_test.go
  • internal/store/sqlite/doc.go
  • internal/store/sqlite/workflow_store.go
  • internal/store/sqlite/workflow_store_test.go
  • internal/store/workflow_store.go

Comment on lines +45 to +59
func (s *FileWorkflowStore) GetVersion(ctx context.Context, name string, version int) (WorkflowVersion, error) {
workflow, err := s.Get(ctx, name)
if err != nil {
return WorkflowVersion{}, err
}
if version != 1 {
return WorkflowVersion{}, ErrWorkflowNotFound
}
return WorkflowVersion{
Name: name,
Version: 1,
Workflow: workflow,
Metadata: workflow.Metadata,
CreatedAt: time.Time{},
}, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

GetVersion fabricates version semantics for updated workflows.

Line 46 reads the current file, and Lines 55-56 hardcode that payload as version 1. After any Update, GetVersion(..., 1) returns the latest content, not historical version 1, which violates the versioned-store contract. If this backend cannot provide history, return a consistent “not found/unsupported” error instead of incorrect version data.

Suggested minimal safe fallback
-func (s *FileWorkflowStore) GetVersion(ctx context.Context, name string, version int) (WorkflowVersion, error) {
-	workflow, err := s.Get(ctx, name)
-	if err != nil {
-		return WorkflowVersion{}, err
-	}
-	if version != 1 {
-		return WorkflowVersion{}, ErrWorkflowNotFound
-	}
-	return WorkflowVersion{
-		Name:      name,
-		Version:   1,
-		Workflow:  workflow,
-		Metadata:  workflow.Metadata,
-		CreatedAt: time.Time{},
-	}, nil
-}
+func (s *FileWorkflowStore) GetVersion(_ context.Context, _ string, _ int) (WorkflowVersion, error) {
+	return WorkflowVersion{}, ErrWorkflowNotFound
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (s *FileWorkflowStore) GetVersion(ctx context.Context, name string, version int) (WorkflowVersion, error) {
workflow, err := s.Get(ctx, name)
if err != nil {
return WorkflowVersion{}, err
}
if version != 1 {
return WorkflowVersion{}, ErrWorkflowNotFound
}
return WorkflowVersion{
Name: name,
Version: 1,
Workflow: workflow,
Metadata: workflow.Metadata,
CreatedAt: time.Time{},
}, nil
func (s *FileWorkflowStore) GetVersion(_ context.Context, _ string, _ int) (WorkflowVersion, error) {
return WorkflowVersion{}, ErrWorkflowNotFound
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/store/file_workflow_store.go` around lines 45 - 59, The GetVersion
method in FileWorkflowStore incorrectly returns the current workflow content
while claiming it is version 1, which violates the versioning contract. Since
this file-based store cannot maintain historical versions (it only stores the
latest state), modify the method to return a consistent error (such as
ErrWorkflowNotFound) for all version requests instead of fabricating version
data. Remove the logic that reads the current workflow and returns it as version
1, and ensure the method either accepts only a specific version number that
indicates "current" or rejects all version requests with an appropriate error
that reflects the store's inability to support versioning.

Comment on lines +100 to +109
func (s *MemoryWorkflowStore) appendVersionLocked(workflow kernel.WorkflowResource) {
name := workflow.Metadata.Name
version := len(s.versions[name]) + 1
s.versions[name] = append(s.versions[name], WorkflowVersion{
Name: name,
Version: version,
Workflow: workflow,
Metadata: workflow.Metadata,
CreatedAt: time.Now().UTC(),
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Version snapshots are currently stored as mutable aliases.

On Line 106, Workflow: workflow stores a shallow copy. If kernel.WorkflowResource contains reference fields (maps/slices), later caller-side mutations can retroactively change historical versions returned by GetVersion. Capture a deep copy before persisting both head/version entries to keep version history immutable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/store/memory_workflow_store.go` around lines 100 - 109, In the
appendVersionLocked method of MemoryWorkflowStore, the Workflow field is storing
a shallow copy of the workflow parameter, which allows mutations to reference
types (maps/slices) within kernel.WorkflowResource to retroactively modify
historical versions. Create a deep copy of the workflow parameter before storing
it in the WorkflowVersion struct to ensure version history immutability. This
deep copy should be created before the append operation and assigned to the
Workflow field in the WorkflowVersion initialization.

Comment on lines +41 to +43
if err := workflowStore.Migrate(ctx); err != nil {
db.Close()
return nil, err

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle database close errors on migration failure.

Line 42 ignores db.Close() errors, which is flagged by errcheck and can hide cleanup failures on startup error paths.

Suggested fix
 	workflowStore := &WorkflowStore{db: db}
 	if err := workflowStore.Migrate(ctx); err != nil {
-		db.Close()
-		return nil, err
+		if closeErr := db.Close(); closeErr != nil {
+			return nil, fmt.Errorf("migrate sqlite workflow store: %v (close db: %w)", err, closeErr)
+		}
+		return nil, err
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err := workflowStore.Migrate(ctx); err != nil {
db.Close()
return nil, err
if err := workflowStore.Migrate(ctx); err != nil {
if closeErr := db.Close(); closeErr != nil {
return nil, fmt.Errorf("migrate sqlite workflow store: %v (close db: %w)", err, closeErr)
}
return nil, err
}
🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 42-42: Error return value of db.Close is not checked

(errcheck)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/store/sqlite/workflow_store.go` around lines 41 - 43, The db.Close()
call on line 42 in the migration error handling path is not checking for errors,
which violates error handling best practices and triggers errcheck warnings.
Modify the error handling to capture the error returned by db.Close() when
workflowStore.Migrate fails, and either log the close error or wrap it together
with the original migration error before returning, ensuring cleanup failures
are not silently ignored.

Source: Linters/SAST tools

@rezaqomy
rezaqomy merged commit cda0012 into master Aug 8, 2026
2 checks passed
@rezaqomy
rezaqomy deleted the agent/sqlite-workflow-persistence branch August 8, 2026 15:33
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.

1 participant