1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# MeterStore — deployment configuration.
#
# `meterstore check` validates this file without connecting to anything, and it
# runs the same cross-field checks the Rust builder does. There is no setting
# reachable from one and not the other.
#
# A `${DATABASE_URL}`-style placeholder in any value is interpolated from the
# environment. A missing variable is an error, not an empty string: a connection
# URL that silently became `postgresql://@/` would fail somewhere far from the
# typo. Comments are left alone, which is why this one can say so.
# --- The hot tier: PostgreSQL holds the recent interval window. ---
[]
= "${DATABASE_URL}"
= 16
# How long a DDL statement waits for a lock before giving up.
#
# PostgreSQL grants locks in arrival order, so a statement waiting for an
# ACCESS EXCLUSIVE lock blocks every reader and writer that arrives behind it.
# Archival detaches a partition on its own schedule, and a detach that queued
# would take ingest down for as long as whatever it waits on runs. With this it
# gives up having changed nothing, and the cycle is reported as deferred.
#
# Raise it on a deployment that reports out of the same tables it writes; every
# second added is a second the whole table can stall for. `0s` disables it,
# which is PostgreSQL's own default and this crate's advice against.
= "3s"
# --- The cold tier: Apache Iceberg holds the history. ---
[]
# "rest" for Polaris, Lakekeeper, Nessie or Gravitino — what every engine
# already speaks, so an external reader needs nothing extra. "sql" for a
# PostgreSQL-backed catalogue on the same database as the hot tier, which is one
# fewer service to run and needs the JDBC catalogue implementation on the far
# side. "s3tables" for an AWS S3 Tables bucket, where `warehouse` below is the
# table bucket ARN and `uri` is unused.
= "sql"
= "${DATABASE_URL}"
# file:// and memory:// are always available. s3://, gs:// and abfss:// need the
# matching `object-store-*` feature, and a scheme whose feature was not compiled
# in is a configuration error rather than a surprise on the first commit.
= "file:///var/lib/meterstore/warehouse"
= "metering"
# Target size for a data file. Larger files mean less manifest to plan against
# and better compression; smaller ones mean finer pruning.
= 536870912
= 4
# --- The subject registry, if any table declares a `subject_column`. ---
#
# Only needed then. A `subject_column` names the column holding pseudonymous
# references to data subjects, and a deployment declaring one without a registry
# is refused at startup: the references would resolve to nothing and an Article
# 17 erasure would have no mapping to destroy.
#
# `erasure_secret` (at least 32 bytes) turns on the suppression list. Without it
# erasure works and does not *stay* worked — once the mapping is gone, nothing
# distinguishes an erased identifier from one never seen, so a pipeline replaying
# old messages registers a fresh reference and silently re-links the subject.
# That is every deployment fed by a message broker.
#
# The key must outlive every erasure and is not recoverable from the database.
#
# [privacy]
# erasure_secret = "${METERSTORE_ERASURE_SECRET}"
# --- One entry per managed table. ---
[[]]
# The physical table holds *every version* of every reading — the audit trail.
# The `_versions` suffix is load-bearing: queries read `readings`, a view that
# keeps only the value currently in force, and summing the physical table
# double-counts every corrected interval.
= "readings_versions"
# "interval" for a Lastgang — energy over [from, to). "point" for a
# Zählerstandsgang — a cumulative register value at an instant.
#
# Never the same table: `value` means two different things, and summing the two
# together gives a number with no meaning that looks exactly like a consumption
# total.
= "interval"
[]
# How far ahead partitions are pre-created. Running out makes inserts fail
# outright, so `partitions_ahead` in `meterstore status` is the number that
# predicts a failure rather than describing one.
= "7d"
[]
# How far behind wall clock archival stays, so a late correction still finds its
# interval in the hot tier where it can be superseded cheaply. It must cover at
# least one `archival_step`, or a window can close while corrections for it are
# still arriving — and they would land below the watermark, where no query looks.
= "7d"
# One window per commit — and the hot table's partition granularity, which is
# the same number. The purge of an archived window is DROP TABLE, not DELETE,
# and that only holds when a window is exactly one partition.
#
# It cannot be changed once a table has archived: the watermark sits on the old
# grid, and a window off that grid names a partition relation nothing creates.
= "1d"
# How long an archived partition is kept before its space is reclaimed.
#
# A query reads the tier boundary when it is *planned* and reads the tiers when
# it *executes*. Archival between the two moves a window across the boundary the
# plan already decided against, so the partition stays readable for this long
# afterwards. Set it above the longest query this deployment runs; the cost is a
# fraction of one partition's disk.
= "1h"
[]
# A snapshot is what makes a past settlement reproducible, so this is a
# compliance decision rather than a disk-space one — which is why expiry is
# opt-in (`meterstore maintain --expire-snapshots`) rather than automatic.
= "10y"
# --- Columns beyond the core schema. ---
#
# `identity = true` puts the column in the merge key, so two rows differing in
# it are different readings. A tenant discriminator declared as an attribute
# instead would let one tenant's correction supersede another's reading.
#
# [[tables.extra_columns]]
# name = "tenant"
# type = "string"
# identity = true
#
# `values` declares a closed vocabulary and renders a `CHECK ... IN (...)`;
# `check` declares an identifier scheme, and the write path parses every value
# with it and stores that scheme's canonical spelling. The two are mutually
# exclusive.
#
# "EIC" 16 characters, ENTSO-E — a Bilanzkreis, a Bilanzierungsgebiet.
# The check character is enforced, so a transposition is caught while
# the delivery that carried it is still in hand.
# "EIC:X" the same, and a party code — which is what a Bilanzkreis is.
# "EIC:Y" is an area (a Bilanzierungsgebiet); the rest of ENTSO-E's
# list is Z W T V A. The two share everything but position 3, so
# without this a Bilanzierungsgebiet stores as a Bilanzkreis and every
# MaBiS grouping over it is wrong. Unlike the check character, this
# half is a regular expression, so the database enforces it as well.
# "MALO" 11 digits — a Marktlokations-ID, check digit enforced.
# "MELO" 33 characters — a Zählpunktbezeichnung. No check digit exists;
# what this adds is the casing, so one Messlokation is one key.
# "BDEW" 13 digits — a Marktpartner-ID. The 13th digit is deliberately NOT
# checked: BDEW's Bildungsvorschrift exempts GS1-issued GLNs.
#
# [[tables.extra_columns]]
# name = "bilanzkreis"
# type = "string"
# identity = false
# check = "EIC:X"
#
# [[tables.extra_columns]]
# name = "lieferant"
# type = "string"
# identity = false
# check = "BDEW"