Schema introduction¶
The schema says what a yamaa file may contain. A field it does not declare, or a value its type does not admit, fails before any data is read. The schema owns each field's shape, kind, default and structural constraints; the rules own what each written item means.
Four file types¶
Every yamaa file is YAML whose first two fields are its header: yamaa, the
oldest language version the file needs, always quoted, and type, which names
one of four file types. Each type has a schema of its own
(Schema files and JSON Schemas).
type |
One file holds |
|---|---|
spec |
One dataset: its inputs, keys, rows, columns, checks and output. A specification may inherit from a parent and include fragments, which are spec files too. |
project |
A study: the specifications of each section, sdtm, adam or send, the function and codelists files they share, the lock of each host language, and the metadata Define-XML needs. |
codelists |
The codelists of one terminology source (Codelists and terminology). |
function |
One project function: its parameters, return type and tests, and the callable that implements it in each host language. |
This specification builds a small ADSL:
yamaa: "1.0"
type: spec
model: ADaM
domain: ADSL
keys: [STUDYID, USUBJID]
input:
DM: input/dm.csv
output:
path: adsl.csv
columns: [STUDYID, USUBJID, AGE, ADULTFL]
columns:
- name: STUDYID
type: str
label: Study Identifier
derivation: DM.STUDYID
- name: USUBJID
type: str
label: Unique Subject Identifier
derivation: DM.USUBJID
- name: AGE
type: int
label: Age
derivation: DM.AGE
- name: ADULTFL
type: str
label: Adult Flag
derivation:
flag:
condition: AGE >= 18
false_value: N
missing: N
An engine of language version 1.M reads a file that declares 1.M or an older
1.x version, and refuses a newer one as unsupported_language_version,
naming the version the file needs
(SCHEMA-2). The version
is text: an unquoted 1.0 is a YAML number and fails as
invalid_field_type. A field or a listed value added after 1.0 names the
version that introduced it, and a file that declares an older version cannot
write it: there it fails as unknown_field or value_not_permitted, naming
that version. Every field of the 1.0 schema exists from 1.0.
A field that names another file checks that file's header too. A parent,
an include entry and an input's schema name specifications, and a
project's functions, codelists and section specs name function,
codelists and specification files. A named file of another type fails there
as unexpected_file_type, and one of a version the engine does not read as
unsupported_language_version. Each layer of a specification, a fragment
included, is checked at the version it declares, and the composed
specification needs the newest version any layer declares
(LAYER-4).
Field kinds¶
Every field has one kind, which says what a bare scalar written in it is. The
schema gives each field its kind, and every field of a JSON Schema carries it
as x-yamaa-kind.
| Kind | A bare scalar in the field is |
|---|---|
expression |
One expression of the closed grammar: a bare name reads a variable, quoted text such as 'DM' is text, and a YAML number, Boolean or null is a literal (EXPR-5). |
name |
One variable name, bare for an output column or qualified, as in DM.AGE; never an expression and never text (NAME-5). |
identifier |
A declared name, such as an input dataset, a column or a named intermediate, or a reference to one. |
value |
A constant: text is text, and YAML null is the missing value where the field's type admits it. |
path |
A local path. A relative one is relative to the file that writes it, unless relative_to: entry selects the entry file (FILE-2) or the path is a link that Define-XML writes, which is relative to the generated document. |
structure |
Not a scalar of its own: the field holds a class, a list or mapping of classes, or a verb or verification. |
In the specification above, each derivation and the flag's condition are
expression fields, so DM.AGE and AGE read variables, while false_value
and missing are value fields, so N is the text N; the true value takes
its default, Y. An expression field spells the same text quoted, as in
derivation: "'N'", and a forgotten quote fails as an unknown name instead
of becoming text. A short form stands for its long form with one field filled,
and its scalar is read as that field: an input written as its path fills its
path, and flag: AGE >= 18 fills the flag's condition.
YAML null is the missing value wherever a value or an expression may be
missing, and it never removes anything: unmapped: null answers a value its
mapping does not list with missing, where leaving unmapped out fails the
run. An identifier, name, path or structure field does not admit null, nor
does a Boolean, a number or a field of listed values: there null fails as
invalid_field_type, and a field is left absent by leaving it out
(LAYER-9). In an expression field,
missing and a number may also be written as grammar text, such as "NULL"
or "5"; the YAML scalar is canonical, and the lint reports the text as
scalar_as_text (Style guide).
The table at the end of this page gives every field's kind, and Schema fields, generated from the schema files, gives each field's type, default and rule as well.
The derivation vocabulary¶
A derivation holds one expression: text of the closed grammar, a YAML number,
Boolean or null, or a mapping that holds one verb of the closed expressions
registry (EXPR-1,
EXPR-5). Text covers what
needs no verb: a reference such as DM.AGE, quoted text, a temporal literal
such as DATE '2024-03-10', NULL, and arithmetic with the numeric
functions (FORMULA-27). The verbs live
in one schema module per family:
| Family | Module | Verbs |
|---|---|---|
| Core selection | schema_expression_core.yaml |
source, first_available, greatest, least, case, flag |
| Vocabulary | schema_expression_mapping.yaml |
mapping, decode, cut |
| Strings | schema_expression_str.yaml |
str_pad, str_substr, str_length, str_extract, str_replace, str_template, str_case |
| Rounding | schema_expression_numeric.yaml |
round_half_away_from_zero, str_decimal |
| Aggregation | schema_expression_aggregate.yaml |
aggregate |
| Dates | schema_expression_date.yaml |
diff, impute, precision, imputation_flag, to_date, date_text, study_day, to_epoch_day, dtc, time_of_day, to_time, shift, duration_value, duration_text, calendar_duration |
| Windows | schema_expression_window.yaml |
row_number, rank, row_value, locf, baseline_flag |
| Collected data | schema_expression_odm.yaml |
odm |
| Extension | schema_expression_function.yaml |
function |
A verb's operand, such as the source of mapping, is a name field that
holds one variable, never a nested expression, so operations compose through
named columns: derive a value into a column, then read the column
(NAME-5). Expressions nest only in case
results, in key match values and in a nearest selection's to, which is
read as a match value is (EXPR-3,
LOOKUP-26).
Layers¶
A specification may name a parent, a spec file it inherits from, which may
name a parent of its own, up to a root layer that names none. Any layer may
also list fragments under include: spec files holding declarations that
several specifications share, such as a column derived the same way in two
datasets. At every layer the order is the same: its parent chain, then its
fragments from left to right, then its own fields, each contribution taking
precedence over the ones before it
(LAYER-3).
A layer need not be complete. Each layer is checked for what it writes, and
the composed specification for required fields. A root field replaces what
came before it whole, while input, intermediates, columns, rows and
windows compose member by member, matched by their key, name or id,
and output and submission compose field by field, so a layer that changes
only output.columns or submission.artifact writes that field alone
(LAYER-5). This study entry inherits
an adverse-event template and includes two fragments:
yamaa: "1.0"
type: spec
model: ADaM
parent: ../templates/ae.yaml
include:
- ../fragments/duration.yaml
- ../fragments/severity.yaml
domain: ADAE
keys: [USUBJID, AESEQ]
output:
path: adae.csv
columns: [USUBJID, AESEQ, ASTDT, AENDT, ADURN, ASEVN]
A fragment declares neither parent nor include, so it composes on
whichever layer includes it; one that does fails as prohibited_construct
(LAYER-4). A relative path a fragment
writes stays relative to the fragment
(LAYER-10). A file contributes once or
not at all: an include entry that reaches a file the composition already
holds fails as duplicate_layer, and one that reaches the layer listing it,
or a layer below that one on the chain, as inheritance_cycle
(LAYER-3). The duration fragment
derives one column from two that the template declares:
yamaa: "1.0"
type: spec
columns:
- name: ADURN
type: int
label: Analysis Duration (N)
derivation:
diff:
type: date
start: ASTDT
end: AENDT
unit: day
closed: both
Excel to yamaa uses one fragment on two templates.
A layer removes an inherited value only by listing its spec path in clear.
Composition removes each listed value before the layer's own fields compose,
and a field with a default takes the default again
(LAYER-9). This layer drops a
column's inherited checks and one entry of an inherited dictionary:
yamaa: "1.0"
type: spec
parent: adsl-template.yaml
clear:
- columns.AGE.verifications
- columns.RACEN.derivation.mapping.dict['NOT REPORTED']
A path names a column by its name, an input or a window by its key, and a
named intermediate or a row template by its id; it writes a key with other
characters than letters, digits, _ and - quoted in brackets, as in
['NOT REPORTED'], and holds no list index, since a list replaces whole. A
path that names nothing the layer inherits, a required or identity field,
yamaa, type or a whole member fails as invalid_clear.
A project file layers the same way: it names one parent project file and
may clear what it inherits, study, environments and each section
composing field by field, and it includes no fragment
(PROJECT-16).
Column order¶
A single file keeps its columns in the order they are written, and that order
must be a dependency order: each column reads only columns written before it
(COLUMN-5). A layered specification, one
that names a parent or includes a fragment, is sorted instead: each column
comes after every column it reads, and of the columns that may come next, the
one contributed earliest comes first
(LAYER-13). Layers cannot place a
column between another layer's columns, so composition orders them.
output.columns is never reordered: it alone sets which columns the output
holds, and in what order.
Every field's kind¶
Each row lists, by kind, the fields of one place in a file. A field written in its short form is listed under the field it fills.
A specification:
| Where | expression |
name |
identifier |
value |
path |
structure |
|---|---|---|---|---|---|---|
| Root | filter |
domain, keys, base |
yamaa, type, model, clear, metadata |
parent, include |
input, output, windows, intermediates, columns, rows, verifications, submission |
|
An input written in full, input.<name> |
ordinal, key |
relative_to, types, encoding, byte_order_mark, empty_string, require_match |
path, schema |
|||
output |
order_by |
columns |
relative_to, decimals |
path, report |
||
| An order term written in full | variable |
direction, nulls |
||||
A window: windows.<name>, or a verb's window written in full |
filter |
group_by, order_by |
||||
| A named intermediate | key, filter, derivations |
order_by |
id, dataset, inner_join, columns |
keep, absent, require_match |
between, nearest, pivot, verifications |
|
between, in a named intermediate, source or aggregate |
value |
lower, upper |
||||
nearest, in a named intermediate |
to |
source |
unit |
|||
pivot, in a named intermediate |
name, value, fields |
|||||
| A named intermediate's derivation written in full | derivation |
type, unconvertible |
||||
| A column | derivation |
name |
type, label, metadata |
verifications, submission |
||
A derivation written in full, in a column, a row template or an aggregate derivation |
value |
unconvertible |
||||
A copy entry in columns |
copy |
|||||
copy |
dataset, columns |
submission |
||||
copy.submission |
core, mandatory, role |
|||||
| A row template | filter, derivations |
group_by |
id, dataset |
The verbs, which have no path fields:
| Verb | expression |
name |
identifier |
value |
structure |
|---|---|---|---|---|---|
source |
filter |
variable, order_by |
keep, absent, require_match |
between |
|
first_available |
sources |
missing |
|||
greatest, least |
sources |
||||
case, in each item |
when, then, otherwise |
||||
flag |
condition |
true_value, false_value, missing |
|||
mapping |
source |
dict, case_sensitive, missing, unmapped |
|||
decode |
source |
codelist |
missing, unmapped |
||
cut |
source |
breaks, labels, closed, missing |
|||
str_pad |
source |
width |
|||
str_substr |
source |
start, length, piece, missing |
|||
str_length |
source |
missing |
|||
str_extract |
source |
pattern, group, missing, no_match |
|||
str_replace |
source |
pattern, replacement, missing |
|||
str_template |
template |
missing |
|||
str_case |
source |
to, delimiters, missing |
|||
round_half_away_from_zero |
source |
digits |
|||
str_decimal |
source |
decimals, width, missing |
|||
aggregate |
key, filter, expr |
group_by, order_by |
separator, distinct, quantile |
between, derivations |
|
An aggregate derivation |
derivation |
type |
|||
diff |
start, end |
type, unit, closed |
|||
impute |
source, not_before, not_after, fallback |
type, month, day, time, invalid |
|||
precision |
source |
type, invalid |
|||
imputation_flag |
source |
part, seconds_collected |
|||
to_date, to_epoch_day, time_of_day, to_time |
source |
||||
date_text |
source |
forms, invalid |
|||
study_day |
source, reference |
||||
dtc |
year, month, day, hour, minute, second |
invalid |
|||
shift |
source, days, amount |
type, unit |
|||
duration_value |
source |
unit, invalid |
|||
duration_text |
source |
unit |
|||
calendar_duration |
start, end |
||||
row_number |
window |
||||
rank |
method |
window |
|||
row_value |
source |
offset |
window |
||
locf |
source |
include_current |
window |
||
baseline_flag |
date, reference, source |
inclusive |
window |
||
odm |
filter |
item, event, form, item_group |
|||
function |
args |
name |
The verifications: a column's not_missing, allowed_values, range,
max_length and matches, the dataset's unique, all_or_none, assert
and row_count, and a named intermediate's unique.
| Verification | expression |
name |
identifier |
value |
|---|---|---|---|---|
| Every verification | id |
severity |
||
not_missing |
when |
max_fraction |
||
allowed_values |
values |
|||
range |
min, max |
|||
max_length |
max |
|||
matches |
pattern |
|||
unique, all_or_none |
columns |
|||
assert |
when, require |
|||
row_count |
filter, when |
group_by |
min, max, min_fraction, max_fraction |
Submission metadata, which has no expression or name fields:
| Where | identifier |
value |
path |
structure |
|---|---|---|---|---|
The root's submission |
domain, standard |
mode, has_no_data, label, class, subclass, structure, repeating, reference_data, non_standard, comment |
artifact |
|
A column's submission |
codelist |
key_sequence, core, mandatory, role, data_type, length, significant_digits, display_format, inventory_vocabulary, method, comment |
origins, value_level |
|
A value_level entry |
id, codelist |
where, label, key_sequence, core, mandatory, role, data_type, length, significant_digits, display_format, inventory_vocabulary, method, comment |
origins |
|
A where condition written in full |
comparator, values |
|||
An origins entry |
documents |
type, source, description |
||
A method written in full |
documents |
name, type, description |
expression |
|
method.expression |
context, code |
|||
A comment written in full |
documents |
text |
||
| A document reference written in full | document |
pages |
||
pages |
type, refs, title |
Project, codelists and function files, which have no expression or name fields:
| Where | identifier |
value |
path |
structure |
|---|---|---|---|---|
| A project's root | yamaa, type, clear |
parent, functions |
environments, codelists, study, sdtm, adam, send |
|
environments |
python, r |
|||
environments.python, environments.r |
lock |
|||
study |
id |
name, description, protocol_name |
||
A section, sdtm, adam or send |
define_version, transport, context, xml_lang, file_oid, originator, source_system_version |
specs, define, stylesheet |
standards, documents, metadata_version |
|
A standards entry |
name, version, status |
|||
A documents entry |
id |
kind, title |
link |
|
metadata_version |
id |
name, description |
||
| A codelists file's root | yamaa, type |
standard, codelists |
||
An inline codelist source in a project's codelists |
standard, codelists |
|||
standard |
name, publishing_set, version |
|||
| A codelist | id |
name, data_type, extensible, alias, format_name |
items, external |
|
An items entry |
value, decode, rank, alias, extended |
|||
external |
dictionary, version, href |
|||
| A function file's root | name, implementations |
yamaa, type, description, returns, may_return_missing, comparison_decimals |
params, tests |
|
A params entry |
name |
type, required, default, accepts_missing |
||
A tests entry |
id, covers |
args, result |
What the schema does not do¶
The schema never defines behavior. It says that cut takes source,
breaks, labels, closed and missing, and which of them are values; the
rules say what cut computes and when it fails. A schema
description links to the rule that owns its field and adds no meaning of its
own.
That split is the whole reading order: the schema tells you what you can
write, the rules tell you what it means, and benchmarks/ shows a runnable
specification that proves both.