rollup_engine 0.1.0 → 0.2.0
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/CHANGELOG.md +8 -0
- data/Rakefile +2 -0
- data/lib/rollup_engine/version.rb +1 -1
- data/the_local/agents/rollup_engine-develop.md +89 -0
- data/the_local/agents/rollup_engine-info.md +38 -0
- data/the_local/agents/rollup_engine-install.md +37 -0
- data/the_local/interface.yml +28 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a0efe78c0847d1d64d6bd4d4a9a77ee2556fc9db0bec6827416ba797daed0b6f
|
|
4
|
+
data.tar.gz: 33c7e071b821dd014cba473c9eef9529bdf083f71470a4a1ccf8bbb59fcaa8a3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3b248d2596cfb795d634092e9c5d6f17ed6b51091a3f7a0450c5428d874a4ac523eb11d8c1667715ea63c18e5d7f4c5dfd3fd0f5a066ec6d4e0f9f55a28634d1
|
|
7
|
+
data.tar.gz: 952b91ee0dd80a5d5e93fda3d2770649ca6791083d261b327708821935ba18af66fddd5c591e299dfba8b64940f8b085d286d20c791ac7691851ab997a0f9978
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,14 @@ All notable changes to this project are documented here, following
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.0] - 2026-09-25
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Ships `rollup_engine-info`, `rollup_engine-install` and `rollup_engine-develop` locals
|
|
13
|
+
for the_local, so host apps that install locals get agents that know this gem.
|
|
14
|
+
|
|
15
|
+
## [0.1.0]
|
|
16
|
+
|
|
9
17
|
### Added
|
|
10
18
|
- Initial gem scaffold: mountable `RollupEngine` Rails engine (source-agnostic; no event
|
|
11
19
|
source dependency).
|
data/Rakefile
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rollup_engine-develop
|
|
3
|
+
description: Use PROACTIVELY for declaring a counted or summed measure, rolling records up into daily, weekly or monthly buckets split by dimension, saving those totals as datapoints, and reading a time series back for a chart or report — MUST BE USED instead of hand-rolling group_by-and-sum code, per-period count queries or a custom stats table.
|
|
4
|
+
tools: Read, Write, Edit, Grep
|
|
5
|
+
scope: rollups — declaring measures, aggregating facts by time grain and dimension, and persisting the results as datapoints
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This local follows the steps below exactly and invents none. Where a step names a decision, it asks the developer rather than picking.
|
|
9
|
+
|
|
10
|
+
## What rollup_engine is
|
|
11
|
+
|
|
12
|
+
A Rails engine that turns a collection of records ("facts") into one number per time bucket, optionally split by dimensions, and saves each number as a `RollupEngine::Datapoint` row. Use this local when an app needs stored counts or totals per day, week, month or other period, or needs to read such a series back.
|
|
13
|
+
|
|
14
|
+
## Interface
|
|
15
|
+
|
|
16
|
+
- `RollupEngine.register_measure` — declares a named measure as a count of facts or a sum of one field.
|
|
17
|
+
- `RollupEngine.recompute` — rolls facts up for a measure and saves one datapoint per bucket, updating existing rows in place.
|
|
18
|
+
- `RollupEngine.reset_measures!` — clears every registered measure, for test isolation.
|
|
19
|
+
- `RollupEngine::Rollup.compute` — rolls facts up for a registered measure and returns the totals as a hash without saving.
|
|
20
|
+
- `RollupEngine::Rollup.count` — counts facts per bucket without a registered measure and returns a hash.
|
|
21
|
+
- `RollupEngine::Rollup.sum` — sums one field of the facts per bucket without a registered measure and returns a hash.
|
|
22
|
+
- `RollupEngine::Datapoint` — the ActiveRecord model holding saved rollups, one row per measure, grain, period and dimension combination.
|
|
23
|
+
- `RollupEngine::Datapoint.series` — returns `[period_start, total]` pairs for one measure over a time range, in period order.
|
|
24
|
+
- `RollupEngine::Datapoint.in_period` — scope limiting datapoints to those whose period starts inside a range.
|
|
25
|
+
|
|
26
|
+
## How to use it
|
|
27
|
+
|
|
28
|
+
1. Put these decisions to the developer before writing code. Do not pick for them.
|
|
29
|
+
- Which records are the facts, and which time attribute places each one in a bucket (for example `created_at` or `paid_at`).
|
|
30
|
+
- Whether each measure counts facts or sums a numeric field, and which field.
|
|
31
|
+
- The grain: `:day`, `:week`, `:month`, `:quarter` or `:year` (also `:hour` or `:minute`). Any name works for which the time value answers `beginning_of_<grain>`. `:week` starts on the app's configured `beginning_of_week`, Monday by default.
|
|
32
|
+
- Which dimensions, if any, to split each bucket by (for example `:region` or `:plan`).
|
|
33
|
+
- When recomputing runs: after each write, in a scheduled job, or on demand.
|
|
34
|
+
|
|
35
|
+
2. Register each measure once at boot, in an initializer such as `config/initializers/rollup_engine.rb`:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
RollupEngine.register_measure(:signups, aggregation: :count)
|
|
39
|
+
RollupEngine.register_measure(:revenue, aggregation: :sum, field: :amount_cents)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- `name` is the key every later call looks the measure up by. Use a symbol and pass that same symbol everywhere; a string and a symbol are different keys, and an unregistered name raises `KeyError`.
|
|
43
|
+
- `aggregation:` is `:count` or `:sum`. Anything else is accepted at registration and raises `ArgumentError` when the measure is first computed.
|
|
44
|
+
- `field:` is required for `:sum` and ignored for `:count`. It is a method name the fact responds to, or a lambda taking the fact and returning a number.
|
|
45
|
+
- Registering the same name again replaces the earlier definition.
|
|
46
|
+
|
|
47
|
+
3. Recompute and save datapoints with `RollupEngine.recompute(measure_name, facts, grain:, time:, by: [])`:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
RollupEngine.recompute(:signups, User.where(created_at: range), grain: :day, time: :created_at)
|
|
51
|
+
RollupEngine.recompute(:revenue, Order.paid.where(paid_at: range), grain: :month, time: :paid_at, by: [:region])
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- `facts` is any enumerable of objects. An ActiveRecord relation is loaded into memory in full, so scope it to the periods being recomputed.
|
|
55
|
+
- `time:` is a method name or a lambda taking the fact and returning a `Time`, `DateTime` or `Date`. Buckets start at the beginning of the grain in that value's own time zone.
|
|
56
|
+
- `by:` is an array of method names. Use symbols or strings here, not lambdas, because each name becomes a key in the saved `dimensions` hash.
|
|
57
|
+
- Each bucket is matched on measure, grain, period start and dimension values. A matching row is updated with the new value and `recomputed_at`; otherwise a row is created. Running it twice on the same facts gives the same rows.
|
|
58
|
+
- It only writes buckets that contain at least one fact. A bucket whose facts were all deleted keeps its old value. When facts can be removed, pass every fact in the affected periods, and delete stale rows for those periods before recomputing if the developer wants them gone.
|
|
59
|
+
- Call it from wherever step 1 decided: a model callback, a job (for example `app/jobs/recompute_signups_job.rb`) or a rake task.
|
|
60
|
+
|
|
61
|
+
4. To get totals without saving, call `RollupEngine::Rollup.compute(facts, measure:, grain:, time:, by: [])` for a registered measure, or `RollupEngine::Rollup.count(facts, grain:, time:, by: [])` and `RollupEngine::Rollup.sum(facts, field, grain:, time:, by: [])` for a one-off. All three return a hash:
|
|
62
|
+
- With `by: []`, each key is the bucket start time: `{ 2026-09-01 00:00 => 42 }`.
|
|
63
|
+
- With dimensions, each key is an array of the bucket start followed by one value per dimension, in `by:` order: `{ [2026-09-01 00:00, "EU"] => 12 }`.
|
|
64
|
+
- `by:` entries here may be method names or lambdas taking the fact.
|
|
65
|
+
- `count` values are integers. `sum` values are whatever adding the field values produces.
|
|
66
|
+
|
|
67
|
+
5. Read saved datapoints back.
|
|
68
|
+
- `RollupEngine::Datapoint.series(:signups, from..to)` returns an array of `[period_start, total]` pairs ordered by period start, with `total` a `BigDecimal`. It adds up every row for that measure whose period starts in the range, across all grains and all dimension values. Record each measure at one grain, or query the model directly when it has more than one.
|
|
69
|
+
- For one grain or one dimension value, query the model:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
RollupEngine::Datapoint.where(measure: "revenue", grain: "month").in_period(from..to).order(:period_start)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- Columns: `measure` (string), `grain` (string), `period_start` (datetime), `dimensions` (JSON hash with string keys, `{}` when there are none), `value` (decimal), `recomputed_at` (datetime).
|
|
76
|
+
- `measure` and `grain` are stored as strings, so compare them with strings in `where`. `series` accepts a symbol or a string.
|
|
77
|
+
- Filter on a dimension value in Ruby, for example `.select { |d| d.dimensions["region"] == "EU" }`, since equality on the JSON column is not supported on Postgres.
|
|
78
|
+
|
|
79
|
+
6. In tests, call `RollupEngine.reset_measures!` in setup or teardown and register the measures each test needs, so a measure registered by one test is not seen by another.
|
|
80
|
+
|
|
81
|
+
## Conventions
|
|
82
|
+
|
|
83
|
+
- Register a measure before calling `recompute` or `Rollup.compute` for it.
|
|
84
|
+
- Use the same symbol for a measure in `register_measure`, `recompute` and `Rollup.compute`.
|
|
85
|
+
- Recompute over every fact in the affected periods, never a partial set; each saved value replaces the old one rather than adding to it.
|
|
86
|
+
- Dimension values must be JSON-serializable, since they are saved in a JSON column.
|
|
87
|
+
- Never set `dimensions_key` on a datapoint. It is derived from `dimensions` on save.
|
|
88
|
+
- Write datapoints through `RollupEngine.recompute`, not by creating `RollupEngine::Datapoint` rows by hand.
|
|
89
|
+
- Adding the gem, copying its migrations and migrating are out of scope here. Use rollup_engine-install for those.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rollup_engine-info
|
|
3
|
+
description: Use to learn what rollup_engine offers — measures, facts, time grains, dimensions, rollups, and the datapoints they are saved as.
|
|
4
|
+
tools: Read
|
|
5
|
+
scope: rollups — declaring measures, aggregating facts by time grain and dimension, and persisting the results as datapoints
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This local explains rollup_engine and the words it uses. It makes no changes and gives no steps.
|
|
9
|
+
|
|
10
|
+
## What rollup_engine is
|
|
11
|
+
|
|
12
|
+
rollup_engine is a Rails engine that turns a collection of records into numbers per time period. You give it facts, name a measure, pick a time grain and optionally some dimensions, and it returns one number per period and dimension combination. It can also save those numbers to its own table, and running the same computation again updates the saved rows rather than adding new ones.
|
|
13
|
+
|
|
14
|
+
It does not care where the facts come from. Anything that can be enumerated and read from works: ActiveRecord rows, plain Ruby objects, or stored events whose values sit inside a JSON payload. Reach for it when an app needs daily, weekly or monthly totals or counts, split by attributes such as channel or plan, and wants those totals stored for charts and reports instead of computed on every request.
|
|
15
|
+
|
|
16
|
+
## Interface
|
|
17
|
+
|
|
18
|
+
rollup_engine declares no commands for this local. Its surface belongs to the other two:
|
|
19
|
+
|
|
20
|
+
- **rollup_engine-install** owns adding the gem to an app and creating its datapoints table.
|
|
21
|
+
- **rollup_engine-develop** owns registering measures, computing rollups, saving them, and reading saved datapoints back as a series.
|
|
22
|
+
|
|
23
|
+
## How to use it
|
|
24
|
+
|
|
25
|
+
- The app does not have rollup_engine yet, or its datapoints table is missing: use **rollup_engine-install**.
|
|
26
|
+
- The app has it and you need to define a measure, compute or save a rollup, or query saved results: use **rollup_engine-develop**.
|
|
27
|
+
|
|
28
|
+
## Conventions
|
|
29
|
+
|
|
30
|
+
- **Fact** — one input record. A fact only needs a time value and whatever the measure and dimensions read from it.
|
|
31
|
+
- **Measure** — a named number defined once by name, such as `:revenue` or `:signups`. Its aggregation is either `count`, which counts facts, or `sum`, which adds up one field of each fact. Measures are registered in memory for the running process, not stored in the database.
|
|
32
|
+
- **Grain** — the size of the time period facts are grouped into, such as `day`, `week`, `month` or `year`. Each fact falls into the period that starts at the beginning of its grain.
|
|
33
|
+
- **Dimension** — an attribute facts are split by, such as a channel. With no dimensions, there is one number per period.
|
|
34
|
+
- **Accessor** — how a time, field or dimension is read from a fact: either the name of a method on the fact, or a lambda that takes the fact and returns the value. A lambda is how values nested in a JSON payload are reached.
|
|
35
|
+
- **Rollup** — the computed result: one value per period, or per period and dimension combination. Computing a rollup saves nothing.
|
|
36
|
+
- **Datapoint** — one saved rollup value, identified by its measure, grain, period start and dimension values. Dimension names are stored as strings, and the order dimensions were given in does not change which row matches.
|
|
37
|
+
- **Recompute** — computing a rollup and saving it as datapoints. Running it again over the same facts updates the existing datapoints in place and records when they were last recomputed.
|
|
38
|
+
- **Series** — the saved values for one measure over a date range, as one total per period start, summed across all dimension combinations.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rollup_engine-install
|
|
3
|
+
description: Use to hook rollup_engine into a project — adding the gem, copying its migrations into the app, and running them to create the datapoints table.
|
|
4
|
+
tools: Bash, Read, Edit
|
|
5
|
+
scope: rollups — declaring measures, aggregating facts by time grain and dimension, and persisting the results as datapoints
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This local follows the steps below exactly and invents none. Where a step names a decision, it asks the developer rather than picking.
|
|
9
|
+
|
|
10
|
+
## What rollup_engine is
|
|
11
|
+
|
|
12
|
+
A Rails engine that aggregates facts into per-period numbers and saves them as datapoints; hook it in when a Rails app needs stored daily, weekly or monthly counts or totals.
|
|
13
|
+
|
|
14
|
+
## Interface
|
|
15
|
+
|
|
16
|
+
- `gem "rollup_engine"` — the Gemfile line that adds the engine to the app.
|
|
17
|
+
- `bin/rails rollup_engine:install:migrations` — copies the engine's migrations into the app's `db/migrate/`.
|
|
18
|
+
- `bin/rails db:migrate` — runs those migrations and creates the `rollup_engine_datapoints` table.
|
|
19
|
+
|
|
20
|
+
## How to use it
|
|
21
|
+
|
|
22
|
+
1. Check the app's Rails version in `Gemfile.lock`. The engine's migrations are written as `ActiveRecord::Migration[8.1]`, so `db:migrate` fails with "Unknown migration version" on anything older than Rails 8.1. If the app is older, stop and tell the developer.
|
|
23
|
+
2. Add `gem "rollup_engine"` to the app's `Gemfile` and run `bundle install`. The gem requires the `json` gem below version 3, so if bundling fails on `json`, report the conflict to the developer and do not change the constraint.
|
|
24
|
+
3. Run `bin/rails rollup_engine:install:migrations`. It copies three migrations into `db/migrate/`, each with the `.rollup_engine.rb` suffix: create the datapoints table, add its dimensions key and unique index, and rename the table to `rollup_engine_datapoints`.
|
|
25
|
+
4. If the app previously used the gem under its old name, `tally`, and already ran its migrations, the first two are skipped as already present and only the rename migration is copied. Confirm with the developer that `tally_datapoints` holds the data they expect to keep before migrating.
|
|
26
|
+
5. Run `bin/rails db:migrate`. It creates `rollup_engine_datapoints` and updates `db/schema.rb` (or `db/structure.sql`).
|
|
27
|
+
6. Commit `Gemfile`, `Gemfile.lock`, the copied migrations and the updated schema file together.
|
|
28
|
+
|
|
29
|
+
There is no initializer to generate, no configuration to set and no route to mount.
|
|
30
|
+
|
|
31
|
+
## Conventions
|
|
32
|
+
|
|
33
|
+
- After migrating, `bin/rails runner 'p ActiveRecord::Base.connection.table_exists?(:rollup_engine_datapoints)'` prints `true`. Anything else means the migrations did not run.
|
|
34
|
+
- `db/schema.rb` shows `rollup_engine_datapoints` with columns `measure`, `grain`, `period_start`, `dimensions`, `dimensions_key`, `value` and `recomputed_at`, and a unique index `index_rollup_engine_datapoints_unique`.
|
|
35
|
+
- After upgrading the gem, run `bin/rails rollup_engine:install:migrations` again, then `bin/rails db:migrate`. Migrations already copied are skipped.
|
|
36
|
+
- Never edit the copied migrations. Changes to the table come from a new gem release.
|
|
37
|
+
- Registering measures, computing rollups and reading datapoints are out of scope here. Use rollup_engine-develop for those.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
scope: rollups — declaring measures, aggregating facts by time grain and dimension, and persisting the results as datapoints
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
- gem "rollup_engine"
|
|
5
|
+
- bin/rails rollup_engine:install:migrations
|
|
6
|
+
- bin/rails db:migrate
|
|
7
|
+
|
|
8
|
+
develop:
|
|
9
|
+
- RollupEngine.register_measure
|
|
10
|
+
- RollupEngine.recompute
|
|
11
|
+
- RollupEngine.reset_measures!
|
|
12
|
+
- RollupEngine::Rollup.compute
|
|
13
|
+
- RollupEngine::Rollup.count
|
|
14
|
+
- RollupEngine::Rollup.sum
|
|
15
|
+
- RollupEngine::Datapoint
|
|
16
|
+
- RollupEngine::Datapoint.series
|
|
17
|
+
- RollupEngine::Datapoint.in_period
|
|
18
|
+
|
|
19
|
+
sources:
|
|
20
|
+
- lib/rollup_engine.rb
|
|
21
|
+
- lib/rollup_engine/engine.rb
|
|
22
|
+
- lib/rollup_engine/measure.rb
|
|
23
|
+
- lib/rollup_engine/rollup.rb
|
|
24
|
+
- lib/rollup_engine/recompute.rb
|
|
25
|
+
- app/models/rollup_engine/datapoint.rb
|
|
26
|
+
- db/migrate/20260605000001_create_tally_datapoints.rb
|
|
27
|
+
- db/migrate/20260605160001_add_dimensions_key_to_tally_datapoints.rb
|
|
28
|
+
- db/migrate/20260921120000_rename_tally_tables_to_rollup_engine.rb
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rollup_engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- tylercschneider
|
|
@@ -76,6 +76,10 @@ files:
|
|
|
76
76
|
- lib/rollup_engine/rollup.rb
|
|
77
77
|
- lib/rollup_engine/version.rb
|
|
78
78
|
- lib/tasks/rollup_engine_tasks.rake
|
|
79
|
+
- the_local/agents/rollup_engine-develop.md
|
|
80
|
+
- the_local/agents/rollup_engine-info.md
|
|
81
|
+
- the_local/agents/rollup_engine-install.md
|
|
82
|
+
- the_local/interface.yml
|
|
79
83
|
homepage: https://github.com/DYB-Development/rollup_engine
|
|
80
84
|
licenses:
|
|
81
85
|
- MIT
|