Masterbelt

masterbelt/masterbelt

Master Data Schema

Synced from main@9490864MarkdownSource

#Master Data Schema

A master is a user-declared, named, externally-populated collection of records.

The master declaration is a top-level declaration of the language. Its surface form is defined together with the other declaration forms in ../language/syntax.md; this document defines its semantics.

#Master Declarations

Masterbelt
master Records {
  record {
    primary id: int,
    name: string,
  }

  source {
    csv "path/of/file.csv" {
      separator: ",",
    }

    csv "another/file.csv"
  }
}

A master declaration introduces a nominal type named by its identifier. The name lives in the same value-and-type identifier name space as a type declaration, so a master declaration and a type declaration with the same name within one file collide and are reported by language/names.

A master declaration is a visible declaration. The optional pub modifier makes the master visible outside its declaring file.

A master declaration may carry documentation comments. They attach to the master declaration as a whole, not to any section inside the body.

#Body Sections

The body of a master declaration is a brace-delimited list of sections. Seven section kinds are defined:

  • record { ... } — the record section. Declares the element record type. Required.
  • source { ... } — the source section. Declares the data sources used to populate the master at import time. Optional.
  • filter { ... } — the filter section. Declares import-time row filters. Optional.
  • validation { ... } — the validation section. Declares record- and collection-level validators run after filtering at export time. Optional. See Validation Section.
  • static { ... } — the static section. Declares constants and methods reachable through the master's surface name (Master.X). Optional.
  • select Name { ... } — a select section. Declares a named projection that maps a subset of the record's fields onto a derived record type. Optional, repeatable. See Select Section.
  • master Name { ... } — a nested master declaration. Reachable as Parent.Name. See Nested Masters.

The five single-kind sections (record, source, filter, validation, static) may each appear at most once. A second occurrence of the same kind is a syntax error reported on the later occurrence as masterbelt.parser.master_section_duplicate. Select sections and nested master declarations may appear any number of times.

A master declaration without a record section is a syntax error reported on the declaration as masterbelt.parser.master_record_missing.

Sections may appear in any order. Tooling-level conventions (formatter) may impose a canonical order; the language itself does not.

#Record Section

The record section declares the master's element record type. Its body is a product type body: a comma-separated list of fields and methods.

Field modifiers readonly and writable carry the same meaning as on any product type. The additional field modifier primary is described in keys.md.

Field names within the record section must be unique; duplicates are reported by the parser exactly as for any product type.

Methods declared inside the record section have the same semantics as product type methods. Method overload resolution and method dispatch are defined in ../language/types.md.

The record section's body must declare at least one field; a record body that declares only methods, or that is empty, is a checker error.

#Source Section

The source section declares external data sources used to populate the master at import time. The body is a sequence of source entries.

EBNF
source_entry  = source_kind string_literal [ source_options ] ;
source_kind   = identifier ;
source_options = "{" [ source_option { "," source_option } [ "," ] ] "}" ;
source_option = identifier ":" expression ;

A source entry begins with a source-kind identifier. The supported source kinds are:

Additional kinds (for example xlsx) will be defined by extending this list. Until a kind appears in this list it is rejected as masterbelt.checker.master_unknown_source_kind.

The path string following the source kind names the resource to import. Resolution of this path against the project's working directory is defined by the importer specification and by ../tooling/configuration.md.

An option list is optional. When written, it is a brace-delimited, comma-separated list of name: value pairs. A trailing comma after the last option is allowed. An empty option list {} is legal and equivalent to omitting the option list entirely.

Each option key is validated against the option schema declared by the source kind's importer specification:

  • An option name not declared by the importer is reported as masterbelt.checker.master_source_option_unknown.
  • An option value whose type does not match the importer's declared option type is reported as masterbelt.checker.master_source_option_type_mismatch.

Duplicate option names within one entry are reported by the parser on the later occurrence as masterbelt.parser.master_source_option_duplicate.

A source section with zero entries is legal at the language level; the driver treats such a master as having no automatic data source.

#Filter Section

The filter section declares row-level filters applied after a record has been read from a source. Each rule has a kind keyword (include or exclude), a reason string, and a brace-delimited body that evaluates a boolean expression against the candidate record:

Masterbelt
master A {
  record { value: int }

  filter {
    include "non-negative" { return self.value >= 0 }
    exclude "outlier"      { return self.value > 100 }
  }
}

#Body Semantics

A filter rule body is a brace-delimited block. The block must return a value of type bool. The receiver of the body is the candidate record, available as the local self typed as the surrounding master's record type.

#Rule Application

Rules apply in source declaration order against each candidate record. The first rule that fails drops the record from the master; no later rule on the same record is evaluated.

  • An include rule drops the record when its body returns false.
  • An exclude rule drops the record when its body returns true.

Every dropped record produces a masterbelt.importer.filter_excluded diagnostic at hint severity. The diagnostic carries the rule's reason string so tooling can surface which rule caused the drop.

#Diagnostics

  • A filter body whose return type is not assignable to bool is reported through the standard return-type-mismatch diagnostic.
  • A filter section that contains no rules is legal at the language level.

#Validation Section

The validation section declares record- and collection-level data quality checks. It is distinct from the filter section:

  • A filter rule drops a record from the master.
  • A validation rule keeps every record and emits a diagnostic.

The two also run at different times: filters run during import while the record set is being assembled; validators run over the final, post-filter dataset. The full order is import → filter → validation → export-write, and validation runs at masterbelt export time before any artifact is written.

The section groups named validators by scope (each per record, all per collection) and asserts conditions over the data. The complete surface form, scoping, execution model, severity, and diagnostics are defined in validation.md.

#Static Section

The static section declares user-defined constants and methods that hang off the master's surface name as static members. The section is a brace-delimited list of const declarations and function declarations; both forms accept the same modifiers and syntax they take at the top level (visibility, doc comments, effects, generics).

Masterbelt
master Items {
  record { primary id: int, value: int }
  source { csv "data/items.csv" }
  static {
    pub const MaxId: int = 9999
    pub fn total(): int {
      let acc: int = 0
      for item in Items.toList() {
        acc = acc + item.value
      }
      return acc
    }
  }
}

The static section's members appear under the master's surface name as static members: a constant is reached as Items.MaxId; a method is reached as Items.total(). Visibility follows the regular pub rule and is independent from the master's own visibility — a pub master may expose private static members and vice versa.

#Scope and Resolution

A static member's body resolves names through the regular module scope: top-level constants, top-level functions, other masters' static members, and the master's own static members are all in scope. Module imports apply the same way they do for any other declaration.

The identifier self is not in scope inside a static section. The static section is detached from any record instance, so self would have no meaningful binding; references resolve through the master's surface name instead (Items.toList(), Items.MaxId). A use of self inside a static body is reported through the regular unbound-name diagnostic.

The master's own record fields are not exposed inside the static section either. A static body that needs to inspect records must iterate through Master.toList() (see Iteration).

#Filter Interaction

A filter rule body MUST NOT call a master static method, because filter rules run during import while the master's record set is still being assembled. The checker rejects such calls with masterbelt.checker.static_call_from_filter. Static constants referenced from a filter body are permitted because they do not consult import state.

#Built-in Members

The static section's user-declared members coexist with the built-in static surface every master exposes (currently the toList() method described under Iteration). Declaring a user member whose name collides with a built-in is reported through the regular duplicate-member diagnostic.

#Diagnostics

  • A duplicate member name within one master's static section is reported as masterbelt.checker.static_member_duplicate.
  • A member name that collides with a built-in master member is reported as masterbelt.checker.static_member_reserved.
  • A static call from inside a filter rule body is reported as masterbelt.checker.static_call_from_filter.

#Select Section

A select section declares a named projection over the master's record. Each select section introduces one projected record type derived from a subset of the master's record fields, together with a query surface that filters, orders, and consumes records through the projected shape.

Masterbelt
master Items {
  record {
    primary id: int,
    name: string,
    count: int,
  }

  select Summary {
    id: id,
    name: name,
  }
}

#Body Form

The body of a select section is a brace-delimited, comma-separated list of target: source field mappings. A trailing comma after the last mapping is allowed. The explicit target: source spelling is required even when the names match; the form is forward-compatible with future revisions that may permit renaming and limited type conversions.

EBNF
master_select_section = "select" identifier "{" [ select_field { "," select_field } [ "," ] ] "}" ;
select_field          = identifier ":" identifier ;

#Constraints

The current revision allows only same-name same-type extraction. Each mapping must:

  • Use the same identifier on both sides (target == source). A renaming mapping is reported as masterbelt.checker.master_select_field_rename_unsupported.
  • Reference a field that exists on the master's record. An unknown source is reported as masterbelt.checker.master_select_source_unknown.
  • Refer to a field with a primitive type (bool, numeric, string). A non-primitive field (a ref<>, list<>, map<>, or nested product) is rejected as masterbelt.checker.master_select_unsupported_field_type.
  • Use a target identifier that is unique within the same projection. A duplicate target is reported as masterbelt.checker.master_select_duplicate_field.

Two select sections under the same master must have distinct names. A duplicate is reported as masterbelt.checker.master_select_duplicate_name. Cross-master name overlap is fine — the codegen identifier carries the master's name as a prefix.

A select section's body with zero mappings is legal at the language level but rejected by the checker through masterbelt.checker.master_select_duplicate_field when an empty body would otherwise be meaningless.

#Generated Names

Each select Name { ... } on master Master { ... } lowers to the following codegen-side identifiers:

Codegen artifactSpelling
Projected record type<Master><Name>Record
Projected relation type<Master><Name>Relation
Projected field builder<Master><Name>Fields (Go / C#); per-target convention for TypeScript
Source-relation projection accessorSelect<Name>() (Go / C#) / select<Name>() (TypeScript)

For master Items { select Summary { id: id, name: name } }, the Go target emits ItemsSummaryRecord, ItemsSummaryRelation, ItemsSummaryFields, and a SelectSummary() accessor on the source ItemsRelation.

#Query Surface

A projected relation exposes the same stage and terminal operations as the source relation (defined in query.md), parametrised on the projected record type. The terminals carry the same asyncable + cancellable + failable triplet the source terminals carry.

The accumulated source-side state (predicates, orderings, skip, take) flows from the source relation into the projected relation at Select<Name>() time, so the projection inherits any filters / orderings the user already chained. Predicates added through the projected relation are typed on the projected record, so authoring Where(ItemsSummaryFields.Name.Eq("alpha")) against an ItemsSummaryRelation is a compile-time match; passing a source-record predicate to the projected relation is a compile-time error.

#Semantics

A projection runs at terminal time, after the source records have been filtered, ordered, skipped, and taken according to the accumulated state. For each surviving source record the runtime copies the named fields into a fresh target record. The relation's underlying record set is never reshaped — projection is a view that materialises on each terminal call.

A projected relation does not re-import data or mutate the source relation. Two terminal calls observe the same record set under the same source-side state, matching the observational behaviour of Iteration and the Query API.

#Scope Section

A scope section declares a named, parameterisable relation query that hangs off the master's relation surface. Unlike the codegen-only Where / OrderBy operators, a scope is authored in Masterbelt source: it names a reusable query fragment that the source program — and, for pub scopes, the generated target API — can apply to a relation.

Masterbelt
master Records {
  record {
    primary id: int,
    name: string,
    age: int,
    gender: int,
  }

  scope adult() {
    return self.where(fn(row) => row.age.ge(20))
  }

  scope gendered(gender: int) {
    return self.where(fn(row) => row.gender.eq(gender))
  }

  pub scope genderedAdult(gender: int) {
    return self.adult().gendered(gender)
  }
}

A scope is declared only inside a master body; there is no top-level or module-level scope. A nested master may declare its own scopes, which surface on that nested master's relation. A scope always returns the declaring master's Relation<M>; it never returns a list<Record>, a nullable, a projected relation, or a joined relation.

#Surface Form

EBNF
master_scope_declaration = [ visibility_modifier ] [ "indexed" ] "scope"
                           identifier "(" [ function_parameters ] ")"
                           ( function_block | scope_expression_body ) ;
scope_expression_body    = "=>" expression ;

A scope declaration carries an optional pub visibility modifier in the same position as every other declaration's visibility, an optional indexed modifier (see Indexed Scopes), the scope keyword, a name, a parameter list, and a body. The pub indexed scope order is the only accepted spelling of the two modifiers; indexed pub scope and scope indexed are syntax errors. Both scope and indexed are context keywords (they remain usable as ordinary identifiers elsewhere); see lexical.md.

A scope never declares an explicit return type — the return type is always the declaring master's Relation<M>, so a return-type annotation is a syntax error — and a scope never carries type parameters.

#Parameters

Scope parameters use the same surface form as regular function parameters: the permitted parameter types, default values, optional / nullable parameters, and the call-site omission rules are identical to those of a regular function. Scope parameters are referenced from the body, including from inside query callbacks.

#Body

A scope body is either a function block or an arrow expression:

  • A block body must return a Relation<M>. let, if, for, assignment, break, continue, and multiple / conditional returns are all allowed; a block body with no return is reported as masterbelt.checker.scope_missing_return.
  • An arrow body (=> expression) requires its expression to evaluate to a Relation<M>.

In both forms a body expression whose type is not the declaring master's Relation<M> is reported as masterbelt.checker.scope_return_type_mismatch.

The body is effect-free: it builds and returns a relation plan and must not inherit failable, cancellable, or asyncable. A scope body — or a query callback inside it — that calls a user function, static, or const carrying any of those effects is reported as masterbelt.checker.scope_forbidden_effect. A scope is therefore never itself failable / cancellable / asyncable. Terminal relation operations (toList, findBy, …) carry those effects and so cannot be reached from a scope body; the constraint is enforced through the effect rule rather than by enumerating forbidden methods.

#self

Inside a scope body, self is the receiver relation: the Relation<M> the scope is applied to. self is a reserved implicit identifier in every context (see lexical.md); a binding that tries to shadow it — as a function or scope parameter, a let / const, or a for / match binding — is rejected, and assigning to self is rejected. When a scope is reached through a scope chain, its self denotes the relation produced by the stages applied earlier in the chain.

#The Relation Query DSL

A scope body reaches the relation query operators that are otherwise codegen-only. The operator vocabulary, the field-handle DSL (row.age.ge(20), row.name.asc()), and the and / or / not combinators are defined in query.md. A scope body uses where, orderBy, thenBy, skip, and take; it never introduces a new query surface such as self.fields.*.

#Calling and Chaining

A scope surfaces as a method on its declaring master's Relation<M> and only on that relation; it cannot be called on another master's relation, on a Record<M>, or on a list<Record>. A scope is reached from a relation receiver:

  • The master's surface name is the base relation entrypoint, so Records.gendered(1) applies gendered to the master's base relation. An imported master's scopes are reachable through the import alias the same way.
  • Within a scope body, self.adult() applies adult to the receiver relation.
  • Scopes chain: self.adult().gendered(gender) and Records.adult().gendered(1) apply each scope in turn, threading the relation produced by one stage into the next scope's self.

Scope chaining does not depend on source order; a scope may chain a scope declared later in the same master. A scope that references itself directly or transitively is reported as masterbelt.checker.cyclic_scope — recursive scopes are forbidden. A scope call whose receiver does not expose the scope is reported as masterbelt.checker.unknown_member, and an argument-count / type mismatch through the general call diagnostics. A query callback referencing a field the record does not declare is reported as masterbelt.checker.scope_unknown_field.

#Visibility

A scope's pub modifier controls generated-target API exposure only. A pub scope is emitted as a method on the generated relation type in every codegen target; a non-pub scope is not emitted into the target API. Visibility does not restrict source-level use: a non-pub scope is still callable from Masterbelt source across module imports, exactly like a non-pub scope inside the declaring module. Generated method casing follows each target's relation-API convention — source genderedAdult is GenderedAdult on Go / C# and genderedAdult on TypeScript. The full per-target shape is defined in codegen/golang.md, codegen/typescript.md, and codegen/csharp.md.

#Indexed Scopes

The indexed modifier marks a scope as a source for SQLite secondary-index inference. It carries no source-level semantics and does not change the generated non-SQLite API; non-SQLite targets ignore it without a diagnostic. When the SQLite backend is in use, the export pipeline infers secondary indexes from the where and order-by stages an indexed scope can produce. The inference rules, the generated DDL, deduplication, naming, and the partial-success / failure diagnostics are defined in export-sqlite.md. indexed and pub are independent: pub scope is exposed but builds no index, indexed scope builds an index but is not exposed, and pub indexed scope does both.

#Naming and Resolution

A scope name is unique within one master; a duplicate is reported as masterbelt.checker.duplicate_scope, and scope overloading is not allowed. A scope name lives in the relation's method namespace: it must not collide with a built-in relation method (where, orderBy, thenBy, skip, take, terminals) or with the master's nested-master / static / function / const names, but it does not collide with a record field of the same name, because Record<M> and Relation<M> are distinct types with distinct namespaces. A collision is reported as masterbelt.checker.scope_name_conflict.

#Diagnostics

  • A duplicate scope name within one master is reported as masterbelt.checker.duplicate_scope.
  • A scope name that collides with a relation method, nested master, static, function, or const is reported as masterbelt.checker.scope_name_conflict.
  • Declaring or assigning self is reported as masterbelt.parser.reserved_identifier, because self is lexically reserved (see lexical.md).
  • A scope body that does not return the declaring master's Relation<M> is reported as masterbelt.checker.scope_return_type_mismatch; a block body with no return as masterbelt.checker.scope_missing_return.
  • A scope body or query callback that inherits a forbidden effect is reported as masterbelt.checker.scope_forbidden_effect.
  • A direct or transitive self-reference is reported as masterbelt.checker.cyclic_scope.
  • A scope call on a receiver that does not expose the scope is reported as masterbelt.checker.unknown_member; an argument mismatch through the general call diagnostics; an unknown field reference inside a callback as masterbelt.checker.scope_unknown_field.

#Nested Masters

A master declaration may contain other master declarations in its body. Each nested master is a full master declaration: it carries its own record, source, filter, static, and further-nested-master sections, and follows the same well-formedness rules as a top-level master.

Masterbelt
master User {
  record { primary id: int, name: string }
  source { csv "data/users.csv" }

  master Friendships {
    record { primary id: int, owner: int, friend: int }
    source { csv "data/friendships.csv" }
  }
}

A nested master is reached through the parent's surface name with a dot: User.Friendships, User.Friendships.toList(), User.Friendships.SomeStatic. The dotted form behaves identically to a top-level master reference; nested masters are nominal types in the same name space as their parent's static members.

#Naming

Two nested masters under the same parent must differ in name. A duplicate is reported as masterbelt.checker.nested_master_duplicate.

A nested master's name must not collide with any of the parent's own static members (constants, methods, or other nested masters). A collision is reported as masterbelt.checker.static_member_duplicate.

#Visibility

A nested master may carry the pub modifier. Visibility is independent from the parent's: a pub master may contain a private nested master, and a private parent may contain a pub nested one. Cross-module references resolve through the dotted form (OtherModule.Parent.Child).

#Codegen

Each codegen target emits nested masters as siblings of top-level masters under a flattened identifier built by concatenating the parent's surface name and the nested master's surface name (UserFriendships for master User { master Friendships { ... } }). When the nesting depth is greater than one, the concatenation extends through every ancestor in declaration order (UserFriendshipsArchive for two levels of nesting).

The flattened identifier is the only name visible at the target level. Record and relation types follow the same naming policy as top-level masters — UserFriendshipsRecord, UserFriendshipsRelation — and the MasterData accessor for a nested master is flat (data.UserFriendships), not a chained property. Cross-references in the generated source see the same flattened identifier; the dotted Masterbelt-side path does not survive into the target.

#Limits

A nested master inherits no behavior from its parent: it carries its own record, sources, filters, statics, and iteration semantics. Records from the parent and records from a nested master are independent collections.

The body of a nested master may declare further-nested masters, with no fixed depth limit at the language level.

#Runtime Model

The Masterbelt surface form master Foo { ... } does not introduce a runtime singleton. Each master decomposes into two distinct runtime concepts:

  • Record — the pure value type for one row, derived from the master's record { ... } section.
  • Relation — the typed query / access surface over the master's records. Methods like iteration, primary-key lookup, and (in later revisions) richer queries hang off the relation.

A program reaches every relation through a MasterData value: the dataset entry that holds one materialised import. MasterData is not a singleton. A host application that needs to swap datasets per client (for example "Client v1 receives this content, Client v2 receives that") constructs and routes distinct MasterData values; the surface program never sees the choice.

The mapping from a Masterbelt-surface master Foo to its runtime parts is fixed:

SurfaceRuntime
Record type used in user code<Master>Record
Query / access surface<Master>Relation
Dataset entryMasterData, with a flat accessor per master
Nested master Parent.Child<ParentChild>Record / <ParentChild>Relation, reached through the same MasterData with a flat accessor (data.parentChild / data.ParentChild per target convention)

The Masterbelt source program does not refer to MasterData. The implementation threads the active dataset through whatever mechanism the codegen target chooses (Go: through context.Context; TypeScript / C#: through a data parameter passed to terminals). A planner writes Items.toList() and the implementation rewrites the call against the active dataset.

The runtime model belongs to codegen targets. The Masterbelt source language never names <Master>Record, <Master>Relation, or MasterData: those identifiers are codegen-side only.

#Primary Key Lookup

Every relation exposes a built-in primary-key lookup operation that returns the record matching a given primary-key value, or a per-target "no match" sentinel when no record matches. The lookup is part of the runtime model; the Masterbelt source program never names it directly.

The method carries the asyncable, cancellable, and failable effects; see Effect Inheritance. Because the Masterbelt source program never names the lookup, no caller has to acknowledge the inheritance; the effect set is baked into each target's FindBy emission directly so backends that can fail (Phase 4 JSON loader, future SQLite, ...) surface that path on the generated signature.

#Naming

The lookup method is always named FindBy (Go / C# PascalCase) or findBy (TypeScript camelCase), regardless of the master's primary-key field name or arity. Composite keys disambiguate through the positional argument list rather than through the method name; relations live on their own type, so there is no overload ambiguity even when several masters share a key field name.

A master rejected by the checker as master_primary_missing (keys.md) has no primary key and therefore no FindBy method.

#Signature

The method takes one positional parameter per primary-key field, named after the field and typed with the field's checked type, in source declaration order. There is no aggregate key struct.

The return shape follows each target's idiomatic "optional value" convention. The failable effect surfaces per the per-target rule (Go: trailing error return; TypeScript / C#: a thrown exception):

TargetReturn
Go(<Master>Record, bool, error) — the second result is true when a record matched and false together with the record's zero value when no record matched. The third result is the backend failure: nil when the lookup completed (whether or not a match was found) and non-nil when the underlying read failed.
TypeScriptPromise<<Master>Record | undefined>findBy is declared async and returns undefined for no match. A backend failure surfaces as a thrown exception.
C#Task<<Master>Record?>FindBy is declared async and returns null for no match. A backend failure surfaces as a thrown exception.

Targets that thread the active dataset through a context.Context (Go) or an analogous per-target slot keep the same threading convention for FindBy as for the other relation methods. The cancellable effect adds the threading slot (ctx context.Context, signal: AbortSignal, CancellationToken cancellationToken); the asyncable effect wraps the return in Promise<R> / Task<R> on TypeScript and C#.

#Semantics

The lookup returns the first record whose primary-key fields are all equal to the supplied arguments using each target's structural equality on the primary-key field's checked type. Equality on primitive primary-key types follows the host language's natural equality (Go and C# ==, TypeScript ===). A master with an empty record set returns the per-target "no match" sentinel for every call.

The lookup is observational: it does not re-import data and does not mutate the relation. A subsequent call observes the same record set as the first call within one run, matching the behaviour of Iteration.

#Query API

Every relation is itself the chainable query surface: stage operations (Where, OrderBy, ThenBy, Skip, Take, projection, join) return a fresh relation, and terminal operations (ToSlice / ToList / toArray, Iter / AsAsyncEnumerable, FindBy, FirstOrDefault, Count, Any) execute the accumulated plan against the active dataset. The cross-target model, the operator vocabulary, and the runtime plan AST are defined in query.md.

#Join Operator

A master whose record carries a ref<Target> field (see Reference Fields) exposes a per-ref join relation on its query surface. Each such field automatically generates a parallel relation that walks the source records, resolves the ref's expanded primary-key fields against the target relation's FindBy, and yields a pair of (left, right) records per successful match. The join's full contract — pair record, pair field builder, joined relation type, INNER JOIN semantics, and how source-side state flows into the join — is defined in query.md.

#JSON Export Format

A project's imported master data is serialised to a single JSON document with the flat shape:

JSON
{
  "items": [
    {"id": 1, "value": 10},
    {"id": 2, "value": 20}
  ],
  "userFriendships": [
    {"owner": 7, "friend": 9}
  ]
}
  • Top-level keys are the masters' flat camelCased identifiers: the same names the per-target MasterData accessor uses (items, userFriendships). A nested master master User { master Friendships { ... } } appears under the flattened-then-camelCased key (userFriendships).
  • Each value is an array of record objects. Records preserve the importer's row order; a master that declared no source section appears with an empty array.
  • Per-record keys are the master's surface field names as written in source (id, name, userId). Records sort their keys lexicographically inside the file so the on-disk shape is deterministic.
  • Primitive values map directly: bool to JSON true/false, string to JSON string, null to JSON null, integers whose absolute value fits in 2^53 to JSON number. Integers outside that range serialise as quoted strings so JavaScript-side consumers receive a lossless representation.
  • Composite values nest naturally: list<T> becomes a JSON array, map<K, V> becomes a JSON object keyed by the entries' rendered keys, a nested product becomes a JSON object whose keys follow the same lexicographic sort rule as a top-level record.
  • The ref<T> field expands to the target master's primary-key fields (field_pk1, field_pk2, ...) before serialisation; nothing in the JSON output reveals the original ref shape.

The format is the same regardless of codegen target. See export-json.md for the exporter contract and codegen/golang.md, codegen/typescript.md, and codegen/csharp.md for the per-target LoadJSON / loadJSON / LoadJson helper signatures.

#Iteration

A master declaration exposes the surface method toList() on the master's name. The call returns every imported record in import order; records dropped by the master's filter section (Filter Section) are not included in the result.

The method carries the asyncable, cancellable, and failable effects; any callable that transitively reaches toList() inherits the same effect set silently per Effect Inheritance. Each codegen target lifts the inherited effects on the surrounding callable's surface (Go: the ctx parameter plus an error second result; TypeScript: async, AbortSignal, Promise<R>; C#: async, CancellationToken, Task<R>). The Masterbelt source program never writes any of these threading slots; the planner just calls Items.toList().

Masterbelt
master Items {
  record { primary id: int, name: string }
  source { csv "data/items.csv" }
}

fn each() {
  for item in Items.toList() {
    use(item.id, item.name)
  }
}

Items.toList() is the surface form. At the target level it lowers to the chainable relation's list terminal (Go Items.ToSlice(ctx); C# Items.ToList(data, cancellationToken); TypeScript items.toArray(data, signal)); the planner never writes the dataset directly. The lowering is documented per target in codegen/golang.md, codegen/typescript.md, and codegen/csharp.md.

A subsequent call to toList() observes the same record set as the first call within one run. Records are not re-imported on each call; the method is a view over the already-materialised collection.

toList() is a static member: it is reached through the master's declaration name (Items.toList()), not through an instance. The master type name itself does not denote a value; only the static method does.

See query.md for the cross-target query model.

#Reserved Keywords

The identifiers master, record, source, filter, include, exclude, primary, static, and select are reserved by the master data schema and cannot be used as identifiers in any position.

The scope section keywords scope and indexed are context keywords: they are matched only at the scope-declaration position inside a master body and remain usable as ordinary identifiers elsewhere. The implicit relation receiver self is fully reserved (see lexical.md).

Source-kind identifiers (for example csv) are not reserved: they are matched only at the source-kind position inside a source section.

Specification