Struct field assignment desugaring
Hamelin automatically expands struct field assignments into complete struct
reconstructions, a process called struct field assignment desugaring. When you
write event.status = "active" where event already contains a struct value,
Hamelin appears to allow you to modify the struct in place. It does this by
expanding the entire struct into individual field bindings, and replacing only
the target field with your new value. This desugaring happens whenever you
assign to a field path where a parent identifier already holds a struct value,
whether that struct came from an array access, field reference, function call,
or even a struct literal. The process ensures you can modify deeply nested data
without manually specifying every field that needs to be preserved.
How struct literals become narrow bindings
Before understanding field assignment desugaring, you need to see how Hamelin handles struct literals in the first place. When you assign a struct literal to an identifier, Hamelin immediately "cracks" it open into individual field bindings rather than storing it as a single struct value. Each field in the literal becomes its own binding in the environment, with the field path as its identifier. No parent binding is created at all - the struct literal simply expands directly into its component field bindings. This flattening happens immediately when the struct literal is assigned, not later when fields are accessed. These narrow bindings behave exactly like any other bindings in the environment, which means they can be individually modified, dropped, or used in expressions.
SET event = {user_id: "alice", action: "login", timestamp: now(), status: "pending"}
// Desugars to:
SET event.user_id = "alice"
| SET event.action = "login"
| SET event.timestamp = now()
| SET event.status = "pending"
The struct literal assignment creates four field bindings without any parent
binding. The event identifier never exists as a binding itself - it's just
the prefix that groups these fields conceptually. Each field binding stands
alone and can be referenced directly as event.user_id or used in expressions.
If you SELECT event later, Hamelin reconstructs the struct from these
individual bindings. This decomposition into narrow bindings makes struct
literals extremely flexible for manipulation.
You can add new fields to this flattened struct simply by creating new
bindings with the appropriate prefix. When you write SET event.ip_address = "192.168.1.1", you're just adding another binding to the environment that
happens to share the event prefix. Hamelin recognizes this pattern and
includes the new field when reconstructing the struct. Similarly, you can
modify existing fields by reassigning them - SET event.status = "success"
simply overwrites the existing event.status binding. The struct literal
decomposition makes these operations natural extensions of regular variable
assignment.
SET event = {user_id: "alice", action: "login", timestamp: now()}
| SET event.session_id = generate_id() // Adds new field
| SET event.action = "authenticated" // Modifies existing field
| DROP event.timestamp // Removes field
// Results in these bindings:
SET event.user_id = "alice"
| SET event.action = "authenticated"
| SET event.session_id = generate_id()
Non-splittable struct expressions
Not all struct values get flattened into narrow bindings like struct literals do. When you access a struct from an array element, field reference, or function call, Hamelin stores it as a single struct-typed binding rather than decomposing it. These "non-splittable" struct expressions maintain their structure as a cohesive unit in the environment. The distinction matters because modifying a field within these structs requires a different approach - this is where field assignment desugaring becomes essential. Without desugaring, you'd have to manually extract every field, modify the one you want, and rebuild the entire struct.
FROM events
| SET first = events[0]
// This creates just one binding:
// first = <struct value from array>
// NOT these bindings:
// first.user_id = ...
// first.action = ...
// etc.
The array access creates a single binding first that contains the entire
struct value. Unlike with struct literals, there are no individual field
bindings for first.user_id, first.action, etc. The struct remains intact
as a single value in the environment. If you want to modify a field within
this struct, you can't just reassign first.action because that binding
doesn't exist yet. This is the key difference between splittable struct
literals and non-splittable struct expressions.
The desugaring transformation
Assigning to a field of a non-splittable struct triggers Hamelin's desugaring
transformation. The assignment first.status = "active" where first
contains a struct value starts the desugaring process. Hamelin recognizes that
first holds a struct and expands the entire assignment into a series of
operations that reconstruct the struct with the modification. The system doesn't
error or create an isolated field - it transforms the single assignment into
multiple bindings. The desugaring creates explicit bindings for every field,
with your target field getting the new value and all other fields preserving
their original values through field references.
FROM events
| SET first = events[0] // first contains {user_id, action, timestamp, status}
| SET first.status = "active"
// Desugars to:
SET first.status = "active"
| SET first.user_id = first.user_id
| SET first.action = first.action
| SET first.timestamp = first.timestamp
The desugaring process transforms one field assignment into multiple bindings
that collectively reconstruct the struct. Your assigned field first.status
gets the value "active" as intended. Every other field in the original
struct gets a binding that references its current value through a field
reference expression. This transformation happens transparently during
compilation, converting your simple field assignment into the equivalent
manual reconstruction.
Multiple field modifications
When you modify multiple fields of the same struct, each modification builds on the previous state. The first modification expands the struct into individual bindings, and subsequent modifications operate on those bindings rather than the original struct. Each field assignment layers on top of the previous changes, creating a natural composition pattern. The order of modifications matters because each desugaring uses the current environment state, which includes bindings created by earlier desugarings. This incremental approach lets you progressively transform structs through a series of field assignments without complex manual reconstruction.
FROM security_logs
| SET entry = logs[0] // {event_id, severity, message, timestamp}
| SET entry.severity = "HIGH"
| SET entry.reviewed = true
| SET entry.reviewer = "alice"
// First assignment desugars to:
SET entry.severity = "HIGH"
| SET entry.event_id = entry.event_id
| SET entry.message = entry.message
| SET entry.timestamp = entry.timestamp
// Second assignment adds:
SET entry.reviewed = true
// Third assignment adds:
SET entry.reviewer = "alice"
The first field assignment triggers a complete desugaring that creates
bindings for all original fields. The entry.severity field gets the new
value "HIGH" while other fields get field reference bindings. When you assign
entry.reviewed = true, Hamelin recognizes that entry already has
field bindings from the desugaring, so it simply adds the new binding without
triggering another desugaring. The third assignment similarly adds the
reviewer field. The final result is a struct with all original fields plus
the two new fields you added.
Nested struct modification
Desugaring handles nested struct modifications by identifying the correct
level to expand based on which identifier holds an actual struct value. When
you write event.metadata.risk_score = 100, Hamelin examines each level of
the path to find where the struct boundary exists. If event holds a struct
value but metadata is just a field within it (not a separate binding), the
desugaring happens at the event level. The system reconstructs the entire
event struct with a modified metadata field that contains your new
risk_score. This process works recursively, handling arbitrary levels of
nesting as long as each level properly resolves to struct types.
FROM security_events
| SET event = events[0] // {id, metadata: {risk_score: 50, category: "auth"}, timestamp}
| SET event.metadata = {risk_score: 100, category: event.metadata.category, flagged: true}
// Desugars to:
SET event.metadata.risk_score = 100
| SET event.metadata.category = event.metadata.category
| SET event.metadata.flagged = true
| SET event.id = event.id
| SET event.timestamp = event.timestamp
Modifying nested structs requires reconstructing the nested struct explicitly
as shown above. The assignment to event.metadata replaces the entire nested
struct with a new one that includes the modifications. The desugaring process
ensures that all other fields of the parent event struct get preserved
through field references. This pattern of replacing entire
nested structs while preserving parent fields is common when working with
deeply nested data structures.
How DROP uses desugaring
The DROP command leverages the same desugaring mechanism but with inverted logic - instead of adding or modifying fields, it reconstructs the struct with specific fields excluded. When you DROP a field from a non-splittable struct, Hamelin expands all fields except the ones being dropped. This creates the same pattern of bindings with field references, but omits the dropped fields entirely. The resulting bindings represent the struct minus the dropped fields. This elegant reuse of the desugaring mechanism makes DROP work consistently with the rest of Hamelin's struct manipulation features.
FROM user_events
| SET user = events[0] // {id, email, password_hash, last_login, preferences}
| DROP user.password_hash, user.email
// Desugars to (includes all fields EXCEPT dropped ones):
SET user.id = user.id
| SET user.last_login = user.last_login
| SET user.preferences = user.preferences
The DROP desugaring creates bindings for every field except password_hash
and email, effectively removing them from the struct. Each preserved field
gets a field reference binding that maintains its current value. If you later
SELECT user, you'll get a struct containing only id, last_login, and
preferences. This approach to dropping fields integrates seamlessly with
other struct operations.
Field ordering in desugared structs
When Hamelin reconstructs a struct after desugaring, modified fields appear first, followed by preserved fields in their original order. This consistent ordering rule makes modifications visible in the struct's schema and helps track what changed. You see this ordering whether modifying fields through assignment or adding new fields to an existing struct. Each modification moves that field to the front of the field order, creating a kind of modification history encoded in the structure itself. While this changes the field order from the original struct, it provides valuable visibility into what transformations have been applied, and since Hamelin fields have nominal equivalence rather than positional equivalence, this reordering is safe.
FROM events
| SET evt = events[0] // Original: {timestamp, user_id, action, session_id, ip_address}
| SET evt.action = "logout"
| SET evt.duration = 30
// Resulting field order:
// {duration, action, timestamp, user_id, session_id, ip_address}
The modified action field and newly added duration field both appear at
the front of the reconstructed struct. The original fields maintain their
relative ordering (timestamp still comes before user_id, which comes
before session_id). This predictable ordering helps when debugging or when
you need to understand what transformations have been applied to a struct. The
consistency of this rule across all struct operations makes Hamelin's behavior
predictable even with complex nested modifications.