🗃️ The information register (Information)
Updated
The registerEngineType: "Information" engine stores a point on the time
axis, not an accumulation. The rule follows 1C's default: one row per combination of
dimensions as of Period; the next row with the same key and a later Period
closes the previous one. The slice is SliceLast / SliceFirst.
This is not a from/to interval: dated intervals with gaps live as a dictionary
or as a link table with an "effective from / to" pair — for example
LT_PriceTypeHistory (prices in the EffectiveFrom / EffectiveTo window).
The difference is fundamental: a slice pulls the last value forward forever,
while a closed window ends and after it there is no value at all.
Information is for a history of "what value was in force on a date" without
that gap.
What it is not#
Accumulation (Standard / FIFO / …) |
Information (Information) |
|---|---|
TR_ + TB_ tables (movements and balances) |
IR_{Name} only |
| A resource is a delta, the balance is summed | A resource is a value (number, string, date, reference, boolean) |
IRegisterMovementService / ITotalsManager |
GetService<IInformationRegisterService>() |
| Posting a document writes movements | The script calls SetAsync itself; cancelling the document deletes the rows by RecorderMetaId |
| Double entry, balance, a totals driver | None of those flags exist |
Information is not part of RegisterEngines.HoldsPhysicalMovements: it has
no TR_, and DocumentPostingService does not look for it among the postable
registers.
The IR_ schema#
MetaId— the PKPeriodDATETIME2— the moment of the slice (not a user dimension)RecorderMetaId— the document that wrote the row (nullable)- the columns of the physical dimensions (
MetaRegisterDimension) - the columns of the resources (
MetaRegisterResource, the type from the EDT) CreatedDateTime/ModifiedDateTime- the unique index:
Period+ the operational, non-informational dimensions - an index on
RecorderMetaId
The row key is Period + the dimensions with isOperational && !isInformational. The same combination → an UPDATE of the resources and of
the recorder. A different Period → an INSERT.
The API and the scripts#
POST /api/registers/{id}/info — Set (upsert)
POST /api/registers/{id}/info/slice-last — { asOf?, dimensions?, skip?, take?, filter?, orderBy? }
POST /api/registers/{id}/info/column-values — distinct SliceLast text values { column, search, take? }
POST /api/registers/{id}/info/slice-first
GET /api/registers/{id}/info/records — ?from=&to=&recorderMetaId=&skip=&take=&filter=&orderBy=
GET /api/registers/{id}/info/{rowMetaId} — one row
DELETE /api/registers/{id}/info/{rowMetaId}
DELETE /api/registers/{id}/info/recorder/{doc}
The journal asks for a window of 40 rows (skip / take), the same way a dictionary list does. take=0 returns the count without the rows. Scripts that omit take still receive the whole slice. Typing in a column's Values filter calls POST .../info/column-values (DISTINCT on the slice, capped) instead of scanning the loaded page.
Reading is Register / Read, writing and deleting are Register / Write. An empty matrix restricts nothing; as soon as a Read row appears on the register, Write is no longer fail-open: an explicit Write grant is required. An explicit deny is a 403.
Set / Delete check the posting boundary the same way
a document's postings do: a Period date that is no later than the closed date is
rejected.
From a script:
var info = GetService<IInformationRegisterService>();
await info.SetAsync(new TaxRate
{
Period = document.DocumentDate,
TaxCode = line.TaxCode,
Rate = line.Rate,
RecorderMetaId = document.MetaId,
});
var slice = await info.SliceLastAsync<TaxRate>(document.DocumentDate,
new Dictionary<string, object?> { ["TaxCode"] = line.TaxCode });
decimal rate = slice[0].Rate;
TaxRate is the generated POCO ([Table("IR_TaxRate")], the class = the name
of the register). The untyped SetAsync("TaxRate", period, dims, values) also
remains.
SliceLast — for each combination of the key, the last row with
Period <= asOf. SliceFirst — the first with Period >= asOf.
Dimensions are references to records#
A dimension that points to master data (an item, a store, a tax, a
counterparty) is typed as a Reference EDT onto a dictionary. In IR_ it is
the record's Guid; SliceLast / SliceFirst return that same Guid. The slice
journal resolves the caption and opens the record's card — like a grouping in a
totals report.
Do not store the item's name as a string ("Widget", "A"). A string is a
scalar, not a relation: the slice will not open a card, and renaming in the
dictionary breaks the key. A scalar String / Decimal / Boolean is only for when
the value has no record of its own (a rate, an exchange rate, a mode flag).
Cancelling a document (ReverseAllMovementsAsync) calls
DeleteByRecorderAsync(documentId) across every Information register.
In an integration test the same operations go through the harness, without resolving the service:
await Db.SetInformationAsync(new TaxRate { Period = date, TaxCode = code, Rate = 15m, RecorderMetaId = doc });
var slice = await Db.SliceLastAsync<TaxRate>(date, dims);
await Db.DeleteInformationAsync("TaxRate", rowId);
The untyped SetInformationAsync("TaxRate", date, dims, values) also remains.
Values are bound by the column's EDT type#
InformationRegisterService converts every value of SetAsync (dimensions and
resources) and every dimension filter of SliceLastAsync / SliceFirstAsync by
the field's EDT type (InformationRegisterValues), not by the look of the value:
| EDT type | Accepts | Bound as |
|---|---|---|
| String, LargeText | anything | text (a Guid or a date as its text) |
| Reference, Guid | Guid, Guid text |
uuid |
| DateTime, Date | DateTime, ISO text |
timestamp |
| Int, Enum | a number or numeric text | integer |
| Decimal | a number or numeric text | decimal |
| Boolean | bool, "true" / "false" |
boolean |
A value that does not convert fails the call with an ArgumentException that
names the field. Guessing from the look of the value would bind a Guid-shaped
string as uuid even on a String column. Postgres then fails with
operator does not exist: character varying = uuid, and only on real data: a
1C Ref_Key, an ISO date kept as text.
Some scripts still shield such values with a leading ~; OneCSyncState in
the Okrasheno client model is one. The shield still works. Removing it needs a
data migration, because the stored keys carry the ~.
The journal in the UI#
The register's menu item and the Journal button in the designer open the
same TotalsWindow. For Information it is a dated-values journal, not a
totals report. The window title is the register's localized caption. A one-line
hint says the window is the slice as of a date; scripts write it, the journal
does not.
The grid is SliceLast on a date (default: now), optionally filtered by
dimensions — one row per key, the value in force. It loads a window and fetches
the next page on scroll. The Values-tab search in a column filter looks up
distinct values on the slice, not the already-loaded page
(POST .../info/column-values). There is no History tab and no create / edit /
delete: Set / Delete stay on IInformationRegisterService and on tests.
History of every IR_ row remains on GET .../info/records for scripts.
SliceFirst stays on the API and in scripts; the journal does not expose it.
Column headers use the UI language (displayCaption). A reference dimension
cell is a relation to a record (a Guid), not merely a caption: clicking it
opens the dictionary card. Related dictionary fields are offered in the column
picker the same way document movements are.
A live example in Sales: the IR AbcClassification (the profile + the Guid of
the item/customer) is written by IAbcClassifier — ABC/XYZ.
A document's movements (DocumentMovementsWindow) also show the IR rows with
RecorderMetaId = the document.
When to create one#
- a rate, an exchange rate, a status, "the value in force on a date";
- the key is known in advance (item, tax, store — references to records, not names) and changes as a whole at a moment in time.
Do not create one if:
- a sum or a balance is needed — that is an accumulation register;
- intervals with gaps and a ban on overlaps are needed — a dictionary with
ValidFrom/ValidTo.
IRegisterMovementService (balances / movements / transactions.Add) does not
accept Information — NotSupportedException. Deleting the register cascades
to the IR_ table and its rows. The assistant reads the slice from IR_, not from
TB_.
POST /api/schema/sync drops the physical tables that no longer belong to a
register:
IR_*whose suffix is no longer the name of any register (a rename, a delete without a cascade);TR_/TB_/TL_/TC_of a register that has becomeInformation;IR_of a register that has become an accumulation register.
Data from a dropped table does not migrate: a change of name or of engine means a new table. The rights on a register are Read / Write (the "Permissions" tab in the designer, the same operations as in Security).