Skip to content

5. Persistence

Contents of DESL 0.2
Property Type Description
format "fbl" Required. The only format of DESL 0.1 and 0.2.
bindings BindingRef[] Required, at least one. One FBL binding per format family a document of this type may be written in, each <document>#<name> (FBL section 2.3), resolved against the specification's own location.
typeMap map binding type → TypeMapEntry How the bindings' types and attributes map onto the metamodel (5.2).
ids IdStrategy How elements get their ids (5.3).
settings map block name → Setting (DESL 0.2) The part of the model a document's registration keeps, by the name of the block each part is kept in (5.5).
doc Doc

5.1 Bindings

  • The bindings MUST read the same model: for the same content, every binding MUST yield the same elements, attributes and findings, so that a document's format never changes what it means.
  • Two bindings MUST NOT claim the same extension or the same file name (FBL section 12.1). A host opening a body uses the binding whose claims take it (FBL section 8.1).
  • Each binding's template is the body of a new document in its format. A host that creates a document MUST let the user choose the format at that moment, and MUST NOT offer to change a document's format afterwards: that would rewrite every byte.
  • A rule of a binding produces elements of the type its type names: an element type of the metamodel, or a type the type map maps.

5.2 Type map

A binding may name its types and attributes as its format does rather than as the metamodel does. typeMap maps each binding type, by its name, to a TypeMapEntry ($defs/TypeMapEntry):

Property Type Description
as "document", "header", "unreadable" or TypeRef Required. What the binding's entries of this type become.
attributes map binding attribute → attribute name or null The binding's attribute names mapped to the metamodel's; unlisted attributes keep their names, and null leaves one out of the model.
hostAttributes string[] Attributes the binding reads and writes that are not attributes of the model.
doc Doc
  • Without an entry, a binding type and its attributes MUST have the names of an element type and its attributes.
  • as: "document" makes the entry's attributes the document's (metamodel.document). "header" and "unreadable" keep the entry out of the model; an unreadable one is reported by std.unreadableEntry (6.5).
  • hostAttributes are not visible in CEL, on the surface or to constraints, and a writer keeps their values.
  • (DESL 0.2) When the body has no entry of a type mapped as document and a transaction sets a document attribute that type binds, the change is planned as an add of such an entry, by its rule's insert; a rule without one refuses the set.

5.3 Identifiers

persistence.ids ($defs/IdStrategy) says how elements are identified: one rule for every type and, in types, rules for single types that override it.

Property Type Description
strategy "uuid-v4", "uuid-v7" (default), "derived" How ids are made: a random UUID, a time-ordered UUID, or computed from the model (5.3.2). A rule in types may also be (DESL 0.2) "counter", counted up per type (5.3.5).
encoding "hex" (default), "base36" The text form of a UUID (5.3.1).
stable bool An id never changes once it is made. Default true. No effect on derived ids.
missing "assign" (default), "ephemeral" What happens to an element read without a usable id (5.3.4). Top level only.
types map TypeRef → IdRule Per-type rules (5.3.2). Top level only.
doc Doc

An IdRule ($defs/IdRule) has strategy, expression (required for derived), ephemeral (bool or Expression), reason (a Reason) and doc, and (DESL 0.2) for counter the properties of section 5.3.5.

5.3.1 Generation

  • Ids MUST be treated as opaque strings: a runtime MUST NOT decode one to recover a UUID, a time or a key. A hand-written id MUST be accepted whatever the strategy. (DESL 0.2) The one exception is the counter strategy, which reads the number of an id that is its prefix followed by digits (5.3.5) and treats every other id as opaque.
  • hex is the RFC 9562 hyphenated lowercase form. base36 is the 128-bit value as an unsigned integer in lowercase base 36, left-padded with 0 to 25 characters.
  • An id for a new element MUST be generated once, when the transaction is first applied. Redoing the transaction MUST re-create the element with the same id, and a redo that would duplicate an id MUST be refused.

5.3.2 Per-type rules and derived ids

  • A rule in types applies to the named type; properties it leaves out are taken from the top-level rule.
  • With derived, an element's id MUST be the value of expression, evaluated in the identity context (9.3), which returns a string. It MUST be recomputed after the document is read and in every transaction before constraints are checked (10.3). An identity expression MUST NOT read the id of any element other than an ancestor of self; a validator MUST reject one that visibly does. A runtime computes ids parents before children.
  • A derived id MUST NOT be altered to make it unique: two elements with one derived id are reported by std.duplicateId (5.3.4).
  • When a derived id changes in a transaction, the runtime MUST rewrite every reference to it in the same transaction.
"ids": { "strategy": "uuid-v4", "encoding": "base36", "stable": true,
  "types": { "Cell": { "strategy": "derived", "expression": "self.parent.id + '/' + self.property.id" } } }

A cell is its row and its property: the cell of row r1 for property p3 is r1/p3, and it stores no id of its own.

5.3.3 Ephemeral ids

An element is ephemeral when its rule's ephemeral is true, or an expression that is true for it. Its id identifies it within one reading of the document only, such as a filter condition numbered by its place.

  • A runtime MUST NOT store a reference by an ephemeral id, or anything else keyed by one.
  • A gesture whose effect would be such a write MUST be refused before it is applied, with the rule's reason or, without one, a runtime sentence saying that the element's identity does not survive re-reading.
  • An ephemeral element MAY be selected, edited, and targeted by findings and operations within the session.

5.3.4 Missing and duplicate ids

  • All elements of one document share one id space.
  • A runtime MUST open a document in which ids are missing or duplicated, and MUST report each case by std.missingId or std.duplicateId (6.5). It MUST NOT refuse the document for it.
  • Of the elements that share an id, the first in reading order (10.2) MUST keep it: every reference resolves to it. The second and later are shown, and are ephemeral until the duplication is resolved.
  • With missing: "assign", an element without a usable id MUST receive a new one from its rule. The new id MUST NOT be written when the document is opened or validated, and MUST be written with the next edit that writes the document. With missing: "ephemeral", such an element is ephemeral.
  • A runtime MUST NOT change an id to resolve a duplicate, except by a user's action.

5.3.5 Counted ids (DESL 0.2)

With strategy: "counter", given per type in types, a type's ids are a prefix and a number counted up per document:

Property Type Description
prefix string Required. What every id of the type starts with: REQ-.
digits int ≥ 1 The least number of digits a new number is written with, padded with 0: 4 gives REQ-0012. A number that needs more is written with more, and no existing id changes. Default 1.
mark attribute name Required. A string attribute of the document (metamodel.document) that holds the last id the strategy gave.
placeholder string What a writer puts after the prefix in place of a number: with NEW, an element written REQ-NEW has no number yet.
  • The number of an id is the integer its digits make, when the id is the prefix followed by digits only: REQ-0012 and REQ-12 have the number 12. They remain different ids. Every other id has no number.
  • A new id is the prefix and one more than the higher of the mark's number and the highest number of any id in the document that has the prefix. The mark is set to the new id in the same transaction, so a number is not given again after the element that carried it is deleted, and an undo that takes the element back takes the mark back with it.
  • A missing mark, or one without a number, reads as no mark: the count continues from the highest number in the document, and the mark is written with the next new id.
  • An element read with the placeholder has no usable id. It is reported by std.missingId, and under missing: "assign" receives a new id as section 5.3.4 says, with the next edit that writes the document, in reading order.
  • A hand-written number is accepted when no other element has the id (5.3.1), and the count continues past it.
  • Duplicates follow section 5.3.4 unchanged. On an element that std.duplicateId reports, a runtime SHOULD offer to give it a new id; that is the user's action section 5.3.4 asks for, and writes the new id in place of the old one, which no reference can name (5.3.4).
  • Counted ids are stable: a runtime MUST NOT change one for any other reason.

5.4 Steps that change two documents

A step that changes two documents at once (a two-way reference between two files, a renamed file that others name) MUST be one undoable step in the host, whose undo restores both bodies, and which writes neither when either edit is refused. FBL keeps one history per body (FBL section 7.1); making the two one step is the host's.

5.5 Settings in the registration (DESL 0.2)

persistence.settings maps a block of a document's registration to part of the model, so that a designer type keeps beside the body what the body's readers do not need. Each key is the name of a block that every binding of persistence.bindings declares in registration.blocks (FBL section 8.1), and each value a Setting ($defs/Setting) in one of two forms:

Form Properties Meaning
A list type, key, value, initial The elements of type are the block's entries, in the block's order: the attribute key is an entry's key and the attribute value its value. initial lists the elements, by their attribute values, that a document has while the block is absent.
A state attribute, of The attribute attribute of the elements of the types of is kept in the block, keyed by the element's id. Only a value that differs from the attribute's default is stored.
  • Values. A value is kept as its written form: a string as itself, an enumeration value as its stored form (4.4), a number as JSON writes it, and a bool as yes or no. FBL refuses a value that is empty or holds : (FBL section 8.4), so the attribute's facets SHOULD rule such values out.
  • One transaction. A change to such an element or attribute is planned as an edit of the registration, in the same transaction as any change of the body it is made with (FBL section 8.4).
  • Absent or complete. A list's block is absent, and the document has the initial elements, or it holds every element of the type: the first change writes the whole list. A state's block holds the values that differ from the default, and none other.
  • Ids. An element of a list's type has a derived id made from its key (5.3.2), so persistence.ids.types MUST give the type a derived rule. A state's block keys an element by its id, so the binding declares it with keys: "ids", and an element whose id is ephemeral (5.3.3) keeps its value for the session only.
  • Actions. The ordinary actions change these elements: create, set, delete and reorder (8.2). No second set of actions exists for them.
  • Without the registration a document has the initial elements of each list and the default of each state, and works as it does with one: deleting the registration loses only what its blocks held.
  • The elements of a list's type are the document's own (they have no parent) and stand in the block's order. A list's binding block is declared with order: "kept", so that a reorder keeps its place.

Referenced from