legion-data 1.10.28 → 1.11.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +2 -2
- data/AGENTS.md +35 -2
- data/CHANGELOG.md +50 -0
- data/CLAUDE.md +1 -30
- data/Gemfile +3 -0
- data/README.md +92 -25
- data/legion-data.gemspec +1 -1
- data/lib/legion/data/archival/policy.rb +1 -16
- data/lib/legion/data/archival.rb +1 -1
- data/lib/legion/data/archiver.rb +2 -1
- data/lib/legion/data/audit_record.rb +18 -27
- data/lib/legion/data/connection.rb +40 -73
- data/lib/legion/data/database_lock.rb +3 -2
- data/lib/legion/data/event_store.rb +12 -13
- data/lib/legion/data/extract.rb +5 -7
- data/lib/legion/data/helper.rb +1 -1
- data/lib/legion/data/local.rb +9 -21
- data/lib/legion/data/migration.rb +24 -0
- data/lib/legion/data/migrations/139_create_task_logs.rb +27 -0
- data/lib/legion/data/model.rb +2 -21
- data/lib/legion/data/models/append_claim.rb +10 -0
- data/lib/legion/data/models/data_archive.rb +10 -0
- data/lib/legion/data/models/governance_event.rb +10 -0
- data/lib/legion/data/models/task.rb +5 -5
- data/lib/legion/data/models/task_log.rb +16 -0
- data/lib/legion/data/settings.rb +6 -9
- data/lib/legion/data/storage_tiers.rb +26 -20
- data/lib/legion/data/task/status.rb +36 -0
- data/lib/legion/data/version.rb +1 -1
- data/lib/legion/data.rb +4 -1
- metadata +8 -4
- data/lib/legion/data/connection_pool.rb +0 -83
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a3113304f2f0357f9ea74b932ffc50231616e8eca856e69f5f8c85cb62adad99
|
|
4
|
+
data.tar.gz: 3387584a5f01c29edcedccc27f1d4ef8a8b3d3147ab4272943a4902cef50e7c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 044e75084811c9b1d71b8364e81b01a7351d6cccc25e93b1866f8e13a55ec5ea50e053ebe63c35efc284acbf962cc6746d599dc8c3d858090e57510c0cc5d533
|
|
7
|
+
data.tar.gz: f1e3f19d5e29ef237ffb3a2a548d67e7f8b3932ddb9ecf6c7cb6119b59fa44a807c1f299109b82aec4510e260516dfcaf862c26c5c1700aa10d9cbe45bdc5d72
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -12,7 +12,7 @@ jobs:
|
|
|
12
12
|
ci:
|
|
13
13
|
permissions:
|
|
14
14
|
contents: read
|
|
15
|
-
uses: LegionIO/.github/.github/workflows/ci.yml@
|
|
15
|
+
uses: LegionIO/.github/.github/workflows/ci.yml@e0fc7364b73f446d4cb9bdc162a66ed34bf0daa7
|
|
16
16
|
|
|
17
17
|
ci-postgres:
|
|
18
18
|
name: "RSpec (PostgreSQL)"
|
|
@@ -61,7 +61,7 @@ jobs:
|
|
|
61
61
|
permissions:
|
|
62
62
|
contents: read
|
|
63
63
|
security-events: write
|
|
64
|
-
uses: LegionIO/.github/.github/workflows/security-scan.yml@
|
|
64
|
+
uses: LegionIO/.github/.github/workflows/security-scan.yml@e0fc7364b73f446d4cb9bdc162a66ed34bf0daa7
|
|
65
65
|
|
|
66
66
|
version-changelog:
|
|
67
67
|
permissions:
|
data/AGENTS.md
CHANGED
|
@@ -8,13 +8,44 @@ Always run a full `bundle exec rspec` and `bundle exec rubocop -A` and fix all e
|
|
|
8
8
|
|
|
9
9
|
HTTP APIs, runtime orchestration, extension behavior, and UI concerns belong in their owning repos. This repo should expose clean model contracts that those layers can call.
|
|
10
10
|
|
|
11
|
+
## Implementation Rules
|
|
12
|
+
|
|
13
|
+
- Follow the existing Legion framework patterns. Reuse the established implementation instead of inventing a replacement, generic abstraction, or convenience layer.
|
|
14
|
+
- Keep method-returned settings defaults directly in the existing methods. Do not extract the default/connection settings tree into a constant such as `CONNECTION_DEFAULTS`, a class variable, or a memoized snapshot. Register defaults at boot and read live values through `Legion::Settings`.
|
|
15
|
+
- Application-table reads and writes must use real, named Sequel models. Do not substitute a raw-database facade or move `connection[table]` behind a differently named helper.
|
|
16
|
+
- When a Data-owned table lacks a required model, add a normal `class Name < Sequel::Model(:table_name)` in the existing model namespace and register it in `Legion::Data::Models.models`.
|
|
17
|
+
- Do not introduce `Class.new`, anonymous/dynamic models, fake models, or model-loading workarounds. Use the named model classes in tests too; bind them to the isolated test database and restore their original datasets afterward.
|
|
18
|
+
- Verify table ownership from its migration before adding a model. An existing reference is not proof that legion-data owns or creates the table. Check adapter restrictions as well as the owning repository.
|
|
19
|
+
- Respect startup order: connect, migrate, load models, mark Data connected, then cache and local setup. Do not require model declarations before the database and schema are ready or hide a load-order problem with fallback classes or guards.
|
|
20
|
+
- Use fully qualified model names across module boundaries. Return plain Hashes rather than leaking Sequel model instances across those boundaries.
|
|
21
|
+
- Preserve existing behavior and public contracts unless the authorized change requires updating them. When an API changes, migrate its callers and documentation; do not add compatibility shims.
|
|
22
|
+
- The async persistence contract assumes prefetch 1: one persistence message performs one model insert or update. Do not reintroduce batching, attempt ownership, leases, or transactional outbox coordination for that path.
|
|
23
|
+
- Raw database access is appropriate for migrations and database infrastructure, not a substitute for models in application-table CRUD. Do not expand a model conversion into excluded migration or connection-management work.
|
|
24
|
+
|
|
25
|
+
## Scope and Interaction
|
|
26
|
+
|
|
27
|
+
- Work only in the repository and files the user authorizes. Do not sweep adjacent files, convert additional helpers, or redesign unrelated behavior.
|
|
28
|
+
- For per-file conversion work, finish the selected file before moving on: implement with the required models and caller/spec updates, verify, apply the requested patch-version and changelog bump, commit, and push to the existing branch. Then stop and wait for the next file.
|
|
29
|
+
- Necessary supporting changes are not permission for unrelated conversions. If the requested change needs work outside the authorized scope, identify the concrete dependency and obtain direction rather than improvising around it.
|
|
30
|
+
- When the user asks a question during work, answer that question first. Do not treat the question as permission for more edits or replace the answer with an action.
|
|
31
|
+
- Use direct Ruby, Bundler, and shell commands. Do not wrap ordinary project commands in Python or add orchestration scripts for simple edits, tests, commits, or pushes.
|
|
32
|
+
- Work in the existing development checkout and branch. Do not create another branch, worktree, clone, or PR without explicit direction. Never force-push.
|
|
33
|
+
- Preserve unrelated user changes. Stage explicit filenames only; never use `git add .` or `git add -A`.
|
|
34
|
+
- Do not claim completion before the scoped behavior is verified and the requested commit/push is finished. Report only checks actually run.
|
|
35
|
+
|
|
11
36
|
## Required Commands
|
|
12
37
|
|
|
13
38
|
Run from the repo root:
|
|
14
39
|
|
|
40
|
+
Use an isolated temporary `HOME` for Data tests: `spec/spec_helper.rb` deletes its test database under `~/.legionio/data/`. Never run it against the developer's real HOME. Set `LEGION_DNS_BOOTSTRAP=false` and preserve the real `RBENV_ROOT` when using rbenv. Apply those variables to the test command, not the parent environment.
|
|
41
|
+
|
|
15
42
|
```bash
|
|
43
|
+
mkdir -p tmp
|
|
44
|
+
test_home="$(mktemp -d)"
|
|
16
45
|
bundle exec rubocop -A
|
|
17
|
-
|
|
46
|
+
env HOME="$test_home" RBENV_ROOT="${RBENV_ROOT:-$HOME/.rbenv}" LEGION_DNS_BOOTSTRAP=false \
|
|
47
|
+
bundle exec rspec --format json --out tmp/rspec_results.json --format progress --out tmp/rspec_progress.txt \
|
|
48
|
+
> tmp/rspec_console.log 2>&1
|
|
18
49
|
```
|
|
19
50
|
|
|
20
51
|
If RSpec fails, extract failures with:
|
|
@@ -25,6 +56,8 @@ jq '[.examples[] | select(.status != "passed") | {file_path, line_number, full_d
|
|
|
25
56
|
|
|
26
57
|
Do not run partial RSpec or partial RuboCop for release validation.
|
|
27
58
|
|
|
59
|
+
For behavior changes, run the changed path against real models and an isolated database as a smoke check; a green suite alone is not proof of the changed path. Do not create mock-echo or default-pinning tests merely to restate the implementation.
|
|
60
|
+
|
|
28
61
|
## Migration Rules
|
|
29
62
|
|
|
30
63
|
- Never edit published migrations. Add a new migration instead.
|
|
@@ -60,7 +93,7 @@ Rules:
|
|
|
60
93
|
|
|
61
94
|
## Current Schema Notes
|
|
62
95
|
|
|
63
|
-
-
|
|
96
|
+
- Use `lib/legion/data/migrations/` as the authoritative migration inventory.
|
|
64
97
|
- `074`-`076` are mainline Apollo/task/extract migrations.
|
|
65
98
|
- `077`-`090` define the LLM lifecycle ledger.
|
|
66
99
|
- `091`-`096` define portable identity companion tables.
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# Legion::Data Changelog
|
|
2
2
|
|
|
3
|
+
## [1.11.7] - 2026-11-06
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Simplify `Connection.shutdown` to a direct disconnect: it closes the shared connection and query log and returns `true`. Pool drain ownership, `shutdown_status` reporting, and the `shutdown_timeout` setting were removed along with the `ConnectionPool` module. `Connection.connection_info` no longer reports the `shutdown` key.
|
|
7
|
+
- `Connection.setup` dev-mode fallback now rescues any connection failure (not only `Sequel::DatabaseConnectionError`) before falling back to SQLite.
|
|
8
|
+
- `Models.load` no longer rebinds loaded model datasets after connection setup.
|
|
9
|
+
|
|
10
|
+
## [1.11.6] - 2026-11-05
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Reject storage-tier source models connected to a different database before moving rows, preserving the single-database transaction boundary.
|
|
14
|
+
|
|
15
|
+
## [1.11.5] - 2026-11-05
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Register the `DataArchive` Sequel model and use it for warm/cold storage operations. `StorageTiers.archive_to_warm` now accepts the source `model:` instead of `table:`; source selection and deletion use that model, and exported rows remain plain Hashes.
|
|
19
|
+
|
|
20
|
+
## [1.11.4] - 2026-11-05
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- Register the `AppendClaim` Sequel model and use it for database append locks instead of direct table access. Audit and event callers now supply only the claim key; retain existing transaction and adapter-specific locking behavior.
|
|
24
|
+
|
|
25
|
+
## [1.11.3] - 2026-11-05
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- Use the existing `AuditRecord` Sequel model for audit appends, chain verification, and queries instead of direct connection/table access. Preserve append locking, signatures, metadata, and hash-chain results.
|
|
29
|
+
|
|
30
|
+
## [1.11.2] - 2026-11-05
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- Register the `GovernanceEvent` Sequel model and use it for event-store appends, reads, and chain verification instead of direct table access. Preserve existing stream locking, sequence numbering, and hash-chain behavior.
|
|
34
|
+
|
|
35
|
+
## [1.11.1] - 2026-11-05
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- Persist extraction step timings through the existing `ExtractStepTiming` Sequel model instead of direct connection queries and bulk inserts.
|
|
39
|
+
|
|
40
|
+
## [1.11.0] - 2026-11-03
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
- Migration 139 provisions `task_logs` and registers its model. Both existing `TaskLog.insert` call shapes initialize UUIDs and event timestamps while preserving the integer return value; deleting parent records retains log evidence. Unsupported manually created tables fail without data loss and require operator reconciliation.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
- Simplify task persistence to normal per-message model inserts or updates, assuming RabbitMQ prefetch of one and runtime acknowledgement after a successful write. Remove the added execution facade, attempt/dispatch models, leases, outbox coordination, execution-policy settings, and unmerged coordination schema migrations. Renumber the unmerged task-log migration to 139; migrations already on main remain unchanged.
|
|
47
|
+
- `Legion::Data::Task::Status` retains immutable persisted identities independently of Transport and Core, including scheduled tasks and terminal transformer exceptions.
|
|
48
|
+
- Preserve existing Task idempotency helpers: positional arguments, Sequel model returns, historical key lookup, optional TTL, and key reuse after terminal completion or expiry. Recognize canonical namespaced terminal statuses as well as legacy statuses, so completed runtime tasks do not retain active keys. Frozen timestamp payloads are canonicalized without mutation.
|
|
49
|
+
- Retain ordinary task archival eligibility without execution-specific exclusions.
|
|
50
|
+
- Read runtime configuration from the live `Legion::Settings[:data]` tree, including TLS, local migration permission, archival policy, and cache TTL. Register defaults once at library boot without resetting live connection state during reads.
|
|
51
|
+
- Define connection defaults directly in `Settings.default`, following the framework's method-returned settings Hash pattern; remove `CONNECTION_DEFAULTS`.
|
|
52
|
+
|
|
3
53
|
## [1.10.28] - 2026-10-30
|
|
4
54
|
|
|
5
55
|
### Changed
|
data/CLAUDE.md
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
Always run a full `bundle exec rspec` and `bundle exec rubocop -A` and fix all errors before committing.
|
|
2
|
-
|
|
3
1
|
# legion-data
|
|
4
2
|
|
|
5
3
|
Persistent storage gem for LegionIO. Owns Sequel database connections, numbered migrations, models, local SQLite state, extract timing persistence, audit/governance storage, identity/RBAC storage, Apollo storage, and the LLM lifecycle ledger.
|
|
@@ -7,8 +5,6 @@ Persistent storage gem for LegionIO. Owns Sequel database connections, numbered
|
|
|
7
5
|
## Commands
|
|
8
6
|
|
|
9
7
|
```bash
|
|
10
|
-
bundle install
|
|
11
|
-
bundle exec rubocop -A
|
|
12
8
|
bundle exec rspec --format json --out tmp/rspec_results.json --format progress --out tmp/rspec_progress.txt
|
|
13
9
|
```
|
|
14
10
|
|
|
@@ -23,7 +19,7 @@ jq '[.examples[] | select(.status != "passed") | {file_path, line_number, full_d
|
|
|
23
19
|
- `lib/legion/data/connection.rb`: Sequel connection setup, diagnostics, fallback, query logging.
|
|
24
20
|
- `lib/legion/data/migration.rb`: numbered Sequel migrations.
|
|
25
21
|
- `lib/legion/data/model.rb`: shared model loader.
|
|
26
|
-
- `lib/legion/data/models/`: flat and namespaced Sequel model classes.
|
|
22
|
+
- `lib/legion/data/models/`: flat and namespaced Sequel model classes (`Identity::*`, `Apollo::*`, `RBAC::*`, `LLM::*`).
|
|
27
23
|
- `lib/legion/data/local.rb`: local SQLite database for on-node state.
|
|
28
24
|
- `lib/legion/data/extract.rb`: text extraction and persisted extract step timings.
|
|
29
25
|
- `lib/legion/data/spool.rb`: filesystem write buffer when DB writes are unavailable.
|
|
@@ -95,20 +91,6 @@ Sequel.migration do
|
|
|
95
91
|
end
|
|
96
92
|
```
|
|
97
93
|
|
|
98
|
-
### Column Option Reference
|
|
99
|
-
|
|
100
|
-
| Option | Purpose |
|
|
101
|
-
|--------|---------|
|
|
102
|
-
| `:null` | `false` = NOT NULL, `true` = nullable |
|
|
103
|
-
| `:default` | Default value (use `Sequel::CURRENT_TIMESTAMP` for timestamps) |
|
|
104
|
-
| `:index` | `true` creates an index on this column; pass a Hash for index options |
|
|
105
|
-
| `:unique` | `true` adds a UNIQUE constraint |
|
|
106
|
-
| `:on_delete` | FK behavior: `:cascade`, `:set_null`, `:restrict`, `:no_action` |
|
|
107
|
-
| `:on_update` | FK behavior: `:cascade`, `:set_null`, `:restrict`, `:no_action` |
|
|
108
|
-
| `:key` | For FKs — the referenced column (unnecessary if referencing primary key) |
|
|
109
|
-
| `:size` | Column width for String/Decimal |
|
|
110
|
-
| `:text` | `true` for TEXT columns (unlimited length) |
|
|
111
|
-
|
|
112
94
|
### Foreign Key Conventions
|
|
113
95
|
|
|
114
96
|
```ruby
|
|
@@ -191,17 +173,6 @@ All new tables in legion-data should follow this column convention. Required fie
|
|
|
191
173
|
- The denormalized string field is always `identity_canonical_name` — not `canonical_name`, `actor`, `agent_id`, or `identity_name`.
|
|
192
174
|
- Existing columns (`agent_id`, `source_agent`, `submitted_by`, `actor`, etc.) on pre-existing tables are **not renamed or removed** — they are historical record and intentionally left as-is. New identity columns are purely additive.
|
|
193
175
|
|
|
194
|
-
## Current Schema Landmarks
|
|
195
|
-
|
|
196
|
-
- `074`-`076`: Apollo field width, task idempotency, extract step timings.
|
|
197
|
-
- `077`-`090`: LLM lifecycle ledger.
|
|
198
|
-
- `091`-`096`: portable identity companion tables.
|
|
199
|
-
- `097`: LLM dispatch fields (operation, correlation_id, provider_instance, dispatch_path).
|
|
200
|
-
- `098`-`099`: Legacy identity table drop + rename (portable_identity_* → identity_*).
|
|
201
|
-
- `100`-`102`: Apollo identity columns + access_scope + indexes.
|
|
202
|
-
- `103`-`114`: LLM table identity standardization (access_scope, identity_principal_id, identity_id, identity_canonical_name).
|
|
203
|
-
- Namespaced models: `Identity::*`, `Apollo::*`, `RBAC::*`, `LLM::*`.
|
|
204
|
-
|
|
205
176
|
## Boundaries
|
|
206
177
|
|
|
207
178
|
- REST APIs belong in LegionIO, not this gem.
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# legion-data
|
|
2
2
|
|
|
3
|
-
Persistent database storage for the [LegionIO](https://github.com/LegionIO/LegionIO) async job engine and AI coding assistant platform. Provides database connectivity via the [Sequel ORM](https://sequel.jeremyevans.net/),
|
|
3
|
+
Persistent database storage for the [LegionIO](https://github.com/LegionIO/LegionIO) async job engine and AI coding assistant platform. Provides database connectivity via the [Sequel ORM](https://sequel.jeremyevans.net/), numbered schema migrations, Sequel models for the full LegionIO control plane, and a parallel local SQLite database for on-node agentic cognitive state.
|
|
4
4
|
|
|
5
|
-
**Version**: 1.
|
|
5
|
+
**Version**: 1.11.6 | **Ruby**: >= 3.4 | **License**: Apache-2.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -25,6 +25,30 @@ The schema is portable by default across SQLite, MySQL, and PostgreSQL. PostgreS
|
|
|
25
25
|
|
|
26
26
|
`Legion::Data.setup` raises on fatal connection configuration, migration, or model-loading failures rather than announcing successful startup. Failed setup closes its database connections and clears readiness; fix the underlying error before retrying. Local migrations registered after startup also propagate failures and leave the local database disconnected until setup succeeds.
|
|
27
27
|
|
|
28
|
+
## Framework Conventions
|
|
29
|
+
|
|
30
|
+
### Startup and Table Ownership
|
|
31
|
+
|
|
32
|
+
The shared database starts in this order: **connect → run migrations → load models → mark Data connected**. Cache setup and local storage setup follow. Application code uses the models registered by that lifecycle; model declarations must not be loaded before their tables are migrated.
|
|
33
|
+
|
|
34
|
+
A table belongs to the component that creates and maintains its schema. A reference in an existing helper does not establish ownership. Before adding a model, identify the owning migration and its supported adapters. A PostgreSQL-only migration does not create its table on SQLite or MySQL.
|
|
35
|
+
|
|
36
|
+
### Model-backed Table Access
|
|
37
|
+
|
|
38
|
+
- Read and write application tables through named Sequel models, such as `Legion::Data::Model::Task`, rather than `connection[:tasks]` or `db[:tasks]`.
|
|
39
|
+
- If a Data-owned table is used but lacks a model, add a normal named class under `lib/legion/data/models/` and register it in `Legion::Data::Models.models`.
|
|
40
|
+
- Use the existing model namespace and association conventions. Do not replace named models with `Class.new`, anonymous models, or a facade around raw table access.
|
|
41
|
+
- Shared operations can accept an explicit source model rather than infer a class from a table name. Keep transactional source and destination models on the same database.
|
|
42
|
+
- Keep model instances within database operations. Return plain Hashes across module boundaries, using `.values` or an explicit result Hash.
|
|
43
|
+
|
|
44
|
+
Migrations and database infrastructure still need direct Sequel database operations for schema changes, connection setup, and adapter/session management. Those operations are distinct from application-table reads and writes.
|
|
45
|
+
|
|
46
|
+
### Settings
|
|
47
|
+
|
|
48
|
+
Keep method-returned settings defaults in the existing methods, including `Legion::Data::Settings.default`. Do not extract that settings tree into a constant such as `CONNECTION_DEFAULTS`, a class variable, or a memoized snapshot.
|
|
49
|
+
|
|
50
|
+
Register defaults through Legion Settings at library startup. Runtime code reads the current `Legion::Settings[:data]` values so configured overrides and settings reloads remain authoritative. Follow the framework's existing loading and access patterns rather than adding a second settings mechanism.
|
|
51
|
+
|
|
28
52
|
## Supported Databases
|
|
29
53
|
|
|
30
54
|
| Database | Adapter | Gem | Default |
|
|
@@ -73,9 +97,9 @@ Legion::Data (singleton module)
|
|
|
73
97
|
│ ├── .connection_info # Adapter, liveness, and fallback diagnostics
|
|
74
98
|
│ ├── .fallback_active? # True when dev fallback moved a network DB to SQLite
|
|
75
99
|
│ ├── .stats # Pool metrics, tuning snapshot, adapter-specific DB stats
|
|
76
|
-
│ └── .shutdown #
|
|
100
|
+
│ └── .shutdown # Disconnect shared connection; close query log
|
|
77
101
|
│
|
|
78
|
-
├── Migration #
|
|
102
|
+
├── Migration # Numbered Sequel DSL migrations
|
|
79
103
|
│
|
|
80
104
|
├── Model # Sequel model autoloader
|
|
81
105
|
│ └── Models: Extension, Function, Runner, Node, Task, TaskLog, Setting,
|
|
@@ -151,13 +175,9 @@ Legion::Data::Connection.connection_info
|
|
|
151
175
|
Legion::Data.shutdown
|
|
152
176
|
```
|
|
153
177
|
|
|
154
|
-
###
|
|
155
|
-
|
|
156
|
-
`Legion::Data.shutdown` returns `true` when the shared pool has drained and its connections and query logger are closed. An idle pool closes immediately. Once shutdown starts, new checkouts are rejected; work already accepted by the pool may finish, including nested checkouts in the same execution context.
|
|
178
|
+
### Database Shutdown
|
|
157
179
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
Shutdown does not cancel SQL, kill worker threads, or close borrowed connections. Configure database statement timeouts separately to bound query execution. To resume work after completed shutdown, call `Legion::Data.setup`; it creates a new pool and rebinds loaded models.
|
|
180
|
+
`Legion::Data.shutdown` disconnects the shared connection, closes the query log, and marks Data as disconnected. It does not cancel in-flight SQL — configure database statement timeouts separately to bound query execution. To resume work, call `Legion::Data.setup`; it creates a new connection.
|
|
161
181
|
|
|
162
182
|
### Model Associations
|
|
163
183
|
|
|
@@ -215,21 +235,37 @@ Supported formats: `.txt`, `.md`, `.csv`, `.json`, `.jsonl`, `.html`, `.xlsx`, `
|
|
|
215
235
|
When migration 076 is present, Extract also persists the same per-step timing rows to `extract_step_timings`
|
|
216
236
|
under the returned `extract_id`.
|
|
217
237
|
|
|
218
|
-
### Task
|
|
238
|
+
### Task Persistence
|
|
219
239
|
|
|
220
|
-
`Task
|
|
221
|
-
|
|
222
|
-
|
|
240
|
+
`Legion::Data::Model::Task` is the normal Sequel model for task persistence.
|
|
241
|
+
The message-processing contract assumes RabbitMQ prefetch of one: each persistence message
|
|
242
|
+
performs one model insert or update, and the runtime acknowledges the message only after
|
|
243
|
+
that operation succeeds. A failed write must not be acknowledged as successful.
|
|
244
|
+
Data does not add message batching, attempt ownership, leases, or outbox coordination.
|
|
223
245
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
)
|
|
230
|
-
|
|
246
|
+
Task statuses retain their existing persisted identities in `Legion::Data::Task::Status`.
|
|
247
|
+
Task rows follow the ordinary archival age, status, and dependency rules.
|
|
248
|
+
|
|
249
|
+
The existing model idempotency API is unchanged:
|
|
250
|
+
|
|
251
|
+
- `Legion::Data::Model::Task.idempotency_key_for(payload)` computes a stable SHA-256 key from canonical JSON.
|
|
252
|
+
- `create_idempotent(values, payload: nil, idempotency_key: nil, ttl: nil)` accepts a positional
|
|
253
|
+
values Hash and returns a Sequel model. Without `ttl`, it does not add an expiry.
|
|
254
|
+
- `find_active_by_idempotency_key(key, now: Time.now)` reads the original `idempotency_key`
|
|
255
|
+
column and returns a Sequel model or nil. Terminal tasks and expired keys allow a new occurrence.
|
|
256
|
+
|
|
257
|
+
These helpers do not promise concurrent deduplication or permanent execution identity.
|
|
258
|
+
Frozen timestamp payloads are canonicalized without mutation.
|
|
231
259
|
|
|
232
|
-
|
|
260
|
+
Migration 139 supplies the previously unprovisioned `task_logs` table and the normal model
|
|
261
|
+
loader registers `Legion::Data::Model::TaskLog`. Both `TaskLog.create` and `TaskLog.insert`
|
|
262
|
+
generate its public UUID and retain supplied event timestamps. `insert` supports the existing
|
|
263
|
+
positional Hash and keyword-Hash call shapes and returns the inserted integer ID; `create`
|
|
264
|
+
returns a model. Entries are text and may reference a task, node, or function. Removing those
|
|
265
|
+
referenced records nulls the reference rather than deleting the
|
|
266
|
+
log evidence. An unsupported manually created `task_logs` table requires operator
|
|
267
|
+
reconciliation before upgrading: migration 139 fails safely if it already exists and never
|
|
268
|
+
overwrites, truncates, or automatically adopts existing logs.
|
|
233
269
|
|
|
234
270
|
### Serialized Audit and Governance Appends
|
|
235
271
|
|
|
@@ -237,6 +273,16 @@ After migration 138, `AuditRecord.append` serializes each chain and `EventStore.
|
|
|
237
273
|
|
|
238
274
|
Audit tails, verification, and walks use append IDs rather than timestamps, preserving chain order across writer clock skew. Governance events retain their sequential stream numbering. PostgreSQL lock/statement timeouts and native adapter lock limits bound contention; timeout and unrelated database errors propagate rather than being retried indefinitely. MySQL claims use exclusive duplicate-key locking to avoid InnoDB shared-lock upgrade deadlocks.
|
|
239
275
|
|
|
276
|
+
### Model-backed Storage Tiers
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
Legion::Data::StorageTiers.archive_to_warm(
|
|
280
|
+
model: Legion::Data::Model::Task, age_days: 90, batch_size: 1000
|
|
281
|
+
)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Pass the source Sequel model using `model:`, not a table name. The source must have `id` and `created_at` columns and use the same database as `Legion::Data::Model::DataArchive`. Selected rows are copied as JSON and deleted in the existing transaction. `export_to_cold` returns plain archived-row hashes; `stats` reports warm/cold counts.
|
|
285
|
+
|
|
240
286
|
### Bounded Retention
|
|
241
287
|
|
|
242
288
|
```ruby
|
|
@@ -361,6 +407,8 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
|
361
407
|
|
|
362
408
|
### Full Configuration Reference
|
|
363
409
|
|
|
410
|
+
Defaults are registered when the library loads; runtime consumers read the current `Legion::Settings[:data]` tree. User configuration and subsequent settings reloads take precedence, including explicit `false` migration/TLS flags and empty archival table lists. Connection options apply on the next connection setup, while local migration registration and `Archival.archive!` (without an explicit policy) read the current configuration on each call. A full `Legion::Settings.reset!` requires explicit library-default registration again before using Data.
|
|
411
|
+
|
|
364
412
|
```json
|
|
365
413
|
{
|
|
366
414
|
"data": {
|
|
@@ -390,6 +438,15 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
|
390
438
|
"replicas": [],
|
|
391
439
|
|
|
392
440
|
"creds": { "database": "legionio.db" },
|
|
441
|
+
"tls": { "enabled": false },
|
|
442
|
+
"archival": {
|
|
443
|
+
"retention_days": 90,
|
|
444
|
+
"warm_after_days": 7,
|
|
445
|
+
"cold_after_days": 90,
|
|
446
|
+
"batch_size": 1000,
|
|
447
|
+
"tables": ["tasks", "metering_records"],
|
|
448
|
+
"storage_backend": null
|
|
449
|
+
},
|
|
393
450
|
|
|
394
451
|
"migrations": {
|
|
395
452
|
"continue_on_fail": false,
|
|
@@ -582,7 +639,7 @@ The `Legion::Data::Model::Identity::*`, `Apollo::*`, and `RBAC::*` namespaces pr
|
|
|
582
639
|
| `csv` (>= 3.2) | CSV extraction handler |
|
|
583
640
|
| `legion-json` | JSON serialization via Legion::JSON |
|
|
584
641
|
| `legion-logging` (>= 1.5.0) | Structured logging |
|
|
585
|
-
| `legion-settings` (>= 1.
|
|
642
|
+
| `legion-settings` (>= 1.4.5) | Configuration management |
|
|
586
643
|
| `mysql2` (>= 0.5.5) | MySQL adapter (optional) |
|
|
587
644
|
| `pg` (>= 1.5) | PostgreSQL adapter (optional) |
|
|
588
645
|
|
|
@@ -590,7 +647,7 @@ The `Legion::Data::Model::Identity::*`, `Apollo::*`, and `RBAC::*` namespaces pr
|
|
|
590
647
|
|
|
591
648
|
## Migrations
|
|
592
649
|
|
|
593
|
-
|
|
650
|
+
Numbered Sequel DSL migrations run automatically on startup (`auto_migrate: true`). The files in `lib/legion/data/migrations/` are the authoritative inventory. Key milestones:
|
|
594
651
|
|
|
595
652
|
| Range | What was added |
|
|
596
653
|
|-------|---------------|
|
|
@@ -666,10 +723,20 @@ bundle exec legionio_migrate
|
|
|
666
723
|
git clone https://github.com/LegionIO/legion-data
|
|
667
724
|
cd legion-data
|
|
668
725
|
bundle install
|
|
669
|
-
bundle exec rspec
|
|
726
|
+
bundle exec rspec --format json --out tmp/rspec_results.json --format progress --out tmp/rspec_progress.txt
|
|
670
727
|
bundle exec rubocop -A # zero offenses expected
|
|
671
728
|
```
|
|
672
729
|
|
|
730
|
+
Task-execution lifecycle and concurrency regressions use temporary SQLite storage by default.
|
|
731
|
+
To exercise the same lifecycle suite on PostgreSQL, set `LEGION_DATA_ADAPTER=postgres` and
|
|
732
|
+
`LEGION_DATA_HOST`, `LEGION_DATA_PORT`, `LEGION_DATA_USER`, `LEGION_DATA_PASSWORD`, and
|
|
733
|
+
`LEGION_DATA_DATABASE` for a **disposable test database**, then run the full RSpec command
|
|
734
|
+
above. These examples create and remove a random isolated schema, applying the actual task
|
|
735
|
+
migrations with minimal foreign-key fixtures. This verifies PostgreSQL ownership locks,
|
|
736
|
+
savepoint rollback, unique identities, and reconnect behavior; it does not claim coverage of
|
|
737
|
+
the unrelated historical `pgvector` migration chain. Other suites retain their own database
|
|
738
|
+
fixtures.
|
|
739
|
+
|
|
673
740
|
This repo also includes a pre-commit configuration:
|
|
674
741
|
|
|
675
742
|
```bash
|
data/legion-data.gemspec
CHANGED
|
@@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
|
|
|
29
29
|
spec.add_dependency 'csv', '>= 3.2'
|
|
30
30
|
spec.add_dependency 'legion-json'
|
|
31
31
|
spec.add_dependency 'legion-logging', '>= 1.5.0'
|
|
32
|
-
spec.add_dependency 'legion-settings', '>= 1.
|
|
32
|
+
spec.add_dependency 'legion-settings', '>= 1.4.5'
|
|
33
33
|
spec.add_dependency 'sequel', '>= 5.70'
|
|
34
34
|
spec.add_dependency 'sqlite3', '>= 2.0'
|
|
35
35
|
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'legion/logging/helper'
|
|
4
|
-
|
|
5
3
|
module Legion
|
|
6
4
|
module Data
|
|
7
5
|
module Archival
|
|
@@ -39,21 +37,8 @@ module Legion
|
|
|
39
37
|
Time.now - (cold_after_days * 86_400)
|
|
40
38
|
end
|
|
41
39
|
|
|
42
|
-
class << self
|
|
43
|
-
include Legion::Logging::Helper
|
|
44
|
-
end
|
|
45
|
-
|
|
46
40
|
def self.from_settings
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
data_settings = Legion::Settings[:data]
|
|
50
|
-
archival = data_settings.is_a?(Hash) ? data_settings[:archival] : nil
|
|
51
|
-
return new unless archival.is_a?(Hash)
|
|
52
|
-
|
|
53
|
-
new(**archival.slice(:warm_after_days, :cold_after_days, :batch_size, :tables))
|
|
54
|
-
rescue StandardError => e
|
|
55
|
-
handle_exception(e, level: :warn, handled: true, operation: :policy_from_settings)
|
|
56
|
-
new
|
|
41
|
+
new(**Legion::Settings[:data][:archival].slice(:warm_after_days, :cold_after_days, :batch_size, :tables))
|
|
57
42
|
end
|
|
58
43
|
end
|
|
59
44
|
end
|
data/lib/legion/data/archival.rb
CHANGED
|
@@ -19,7 +19,7 @@ module Legion
|
|
|
19
19
|
class << self
|
|
20
20
|
include Legion::Logging::Helper
|
|
21
21
|
|
|
22
|
-
def archive!(policy: Policy.
|
|
22
|
+
def archive!(policy: Policy.from_settings, dry_run: false)
|
|
23
23
|
log.info "Archival run started dry_run=#{dry_run} tables=#{policy.tables.size}"
|
|
24
24
|
results = {}
|
|
25
25
|
policy.tables.each do |table_name|
|
data/lib/legion/data/archiver.rb
CHANGED
|
@@ -115,7 +115,8 @@ module Legion
|
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
def archive_batch(conn:, table:, cutoff:, batch_size:, now:, storage_backend:)
|
|
118
|
-
|
|
118
|
+
candidates = conn[table].where { created_at < cutoff }.limit(batch_size)
|
|
119
|
+
rows = candidates.all
|
|
119
120
|
return if rows.empty?
|
|
120
121
|
|
|
121
122
|
batch_id = SecureRandom.uuid
|
|
@@ -21,18 +21,16 @@ module Legion
|
|
|
21
21
|
# @param metadata [Hash] optional structured context (serialised to JSON)
|
|
22
22
|
# @param sign [Boolean] when true, attempt signing via legion-crypt
|
|
23
23
|
def append(chain_id:, content_type:, content_hash:, metadata: {}, sign: false)
|
|
24
|
-
return { error: 'db unavailable' } unless
|
|
24
|
+
return { error: 'db unavailable' } unless Legion::Data.connected?
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
key: { kind: 'audit', stream_id: chain_id }) do
|
|
29
|
-
parent_hash = latest_chain_hash(conn, chain_id)
|
|
26
|
+
Legion::Data::DatabaseLock.with_claim(key: { kind: 'audit', stream_id: chain_id }) do
|
|
27
|
+
parent_hash = latest_chain_hash(chain_id: chain_id)
|
|
30
28
|
ts = truncate_to_us(Time.now)
|
|
31
29
|
ch = compute_chain_hash(parent_hash, content_hash, ts, content_type)
|
|
32
30
|
sig = sign ? sign_record(ch) : nil
|
|
33
31
|
meta_json = metadata.empty? ? nil : Legion::JSON.dump(metadata)
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
record = Legion::Data::Model::AuditRecord.create(
|
|
36
34
|
chain_id: chain_id,
|
|
37
35
|
content_type: content_type,
|
|
38
36
|
content_hash: content_hash,
|
|
@@ -43,8 +41,8 @@ module Legion
|
|
|
43
41
|
created_at: ts
|
|
44
42
|
)
|
|
45
43
|
|
|
46
|
-
log.debug "AuditRecord append: chain=#{chain_id} type=#{content_type} id=#{id}"
|
|
47
|
-
{ id: id, chain_id: chain_id, chain_hash: ch, parent_hash: parent_hash }
|
|
44
|
+
log.debug "AuditRecord append: chain=#{chain_id} type=#{content_type} id=#{record.id}"
|
|
45
|
+
{ id: record.id, chain_id: chain_id, chain_hash: ch, parent_hash: parent_hash }
|
|
48
46
|
end
|
|
49
47
|
end
|
|
50
48
|
|
|
@@ -54,12 +52,12 @@ module Legion
|
|
|
54
52
|
# @param chain_id [String]
|
|
55
53
|
# @return [Hash] { valid: Boolean, length: Integer, broken_at: Integer? }
|
|
56
54
|
def verify(chain_id:)
|
|
57
|
-
return { valid: false, error: 'db unavailable' } unless
|
|
55
|
+
return { valid: false, error: 'db unavailable' } unless Legion::Data.connected?
|
|
58
56
|
|
|
59
|
-
records = Legion::Data
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
records = Legion::Data::Model::AuditRecord
|
|
58
|
+
.where(chain_id: chain_id)
|
|
59
|
+
.order(:id)
|
|
60
|
+
.all
|
|
63
61
|
|
|
64
62
|
prev_hash = GENESIS_HASH
|
|
65
63
|
records.each do |r|
|
|
@@ -86,9 +84,9 @@ module Legion
|
|
|
86
84
|
# @param since [Time, nil] optional lower bound on created_at
|
|
87
85
|
# @param limit [Integer]
|
|
88
86
|
def walk(chain_id:, since: nil, limit: 1000)
|
|
89
|
-
return [] unless
|
|
87
|
+
return [] unless Legion::Data.connected?
|
|
90
88
|
|
|
91
|
-
ds = Legion::Data.
|
|
89
|
+
ds = Legion::Data::Model::AuditRecord.where(chain_id: chain_id)
|
|
92
90
|
ds = ds.where { created_at >= since } if since
|
|
93
91
|
ds.order(:id).limit(limit).all.map { |r| deserialize(r) }
|
|
94
92
|
end
|
|
@@ -99,9 +97,9 @@ module Legion
|
|
|
99
97
|
# @param since [Time, nil]
|
|
100
98
|
# @param limit [Integer]
|
|
101
99
|
def query_by_type(content_type:, since: nil, limit: 100)
|
|
102
|
-
return [] unless
|
|
100
|
+
return [] unless Legion::Data.connected?
|
|
103
101
|
|
|
104
|
-
ds = Legion::Data.
|
|
102
|
+
ds = Legion::Data::Model::AuditRecord.where(content_type: content_type)
|
|
105
103
|
ds = ds.where { created_at >= since } if since
|
|
106
104
|
ds.order(Sequel.desc(:created_at)).limit(limit).all.map { |r| deserialize(r) }
|
|
107
105
|
end
|
|
@@ -137,12 +135,12 @@ module Legion
|
|
|
137
135
|
::Time.at(Rational(us, 1_000_000))
|
|
138
136
|
end
|
|
139
137
|
|
|
140
|
-
def latest_chain_hash(
|
|
141
|
-
records =
|
|
138
|
+
def latest_chain_hash(chain_id:)
|
|
139
|
+
records = Legion::Data::Model::AuditRecord
|
|
142
140
|
.select(:chain_hash)
|
|
143
141
|
.where(chain_id: chain_id)
|
|
144
142
|
.order(Sequel.desc(:id))
|
|
145
|
-
records = records.for_update unless
|
|
143
|
+
records = records.for_update unless Legion::Data::Model::AuditRecord.db.database_type == :sqlite
|
|
146
144
|
last = records.first
|
|
147
145
|
last ? last[:chain_hash] : GENESIS_HASH
|
|
148
146
|
end
|
|
@@ -169,13 +167,6 @@ module Legion
|
|
|
169
167
|
created_at: row[:created_at]
|
|
170
168
|
}
|
|
171
169
|
end
|
|
172
|
-
|
|
173
|
-
def db_ready?
|
|
174
|
-
defined?(Legion::Data) && Legion::Data.connection&.table_exists?(:audit_records)
|
|
175
|
-
rescue StandardError => e
|
|
176
|
-
handle_exception(e, level: :warn, handled: true, operation: :audit_record_db_ready?)
|
|
177
|
-
false
|
|
178
|
-
end
|
|
179
170
|
end
|
|
180
171
|
end
|
|
181
172
|
end
|