Skip to content

Structs and Arrays

ConvergeDB supports two composite field types: structs (one level of nesting) and arrays (variable-length collections with a declared maximum).

A struct field embeds a group of related sub-fields within an entity. Define the sub-struct with [ConvergenceStruct]:

[ConvergenceStruct]
public partial struct InventorySlot
{
[Field(0)] public ulong ItemId { get; set; }
[Field(1)] public uint Quantity { get; set; }
}

Then use it as a field type in an entity:

[ConvergenceEntity("Character")]
public partial struct Character
{
[Field(0)] public ulong CharacterId { get; set; }
[Field(1)] public InventorySlot PrimaryWeapon { get; set; }
[Field(2)] public InventorySlot SecondaryWeapon { get; set; }
public ReadOnlyMemory<byte> EntityId { get; init; }
}

Sub-struct fields may only contain:

  • Scalar types (ulong, float, bool, etc.)
  • Fixed-length string with MaxLength and FixedLength = true
  • Fixed-length byte[] with MaxLength and FixedLength = true

Nested structs within structs are not supported. VarString, VarBytes, and arrays within structs are not supported. This keeps the storage layout flat and predictable.

Array fields hold a variable number of elements up to a declared maximum. Use the MaxCount parameter on the [Field] attribute:

[ConvergenceEntity("Recipe")]
public partial struct Recipe
{
[Field(0)] public ulong RecipeId { get; set; }
[Field(1, MaxLength = 32)] public string Name { get; set; }
[Field(2)] public uint CraftTimeMs { get; set; }
[Field(3, MaxCount = 20)] public ulong[] IngredientIds { get; set; }
public ReadOnlyMemory<byte> EntityId { get; init; }
}

Arrays can contain:

  • Any scalar type (ulong[], int[], float[], etc.)
  • Fixed-length string with MaxLength
  • Fixed-length byte[] with MaxLength
  • [ConvergenceStruct] types

Arrays of arrays are not supported.

Combine struct definitions with array fields for collections of complex elements:

[ConvergenceStruct]
public partial struct Ingredient
{
[Field(0)] public ulong ItemId { get; set; }
[Field(1)] public uint Count { get; set; }
}
[ConvergenceEntity("Recipe")]
public partial struct Recipe
{
[Field(0)] public ulong RecipeId { get; set; }
[Field(1, MaxLength = 32)] public string Name { get; set; }
[Field(2)] public uint CraftTimeMs { get; set; }
[Field(3, MaxCount = 20)] public Ingredient[] Ingredients { get; set; }
public ReadOnlyMemory<byte> EntityId { get; init; }
}

Arrays, VarString, and VarBytes fields all use a two-zone row layout. The entity row is split into two regions:

  1. Fixed zone — all scalar fields, fixed-length strings/bytes, structs, and a 4-byte directory entry for each variable-length field. The fixed zone has a constant size for every entity of a kind.

  2. Data zone — packed variable-length data. Array elements are stored contiguously at the actual element count, not the declared MaxCount. VarString and VarBytes store only the actual byte content.

Fixed zone Data zone
┌──────────────┬───────────────────┐ ┌──────────────────────────────┐
│ scalar fields │ directory entries │ │ packed array/varstring data │
│ (fixed size) │ (4B per VL field)│ │ (actual content only) │
└──────────────┴───────────────────┘ └──────────────────────────────┘

Each directory entry is 4 bytes: a u16 element count and a u16 data offset into the data zone.

This means entities with small arrays or short strings do not pay the cost of the full MaxCount or MaxLength allocation. An inventory with 10 of 250 possible slots stores only the 10 actual elements.

For kinds with no variable-length fields (no arrays, no VarString, no VarBytes), the data zone is empty and the layout is identical to a simple fixed-size row.

The maximum element count per array field is 1024. See Limits.

Arrays are patched as whole values. If a PATCH includes an array field, the entire array is replaced. Element-level patching (updating a single element within an array) is not supported. To update one element, read the current array, modify the element, and patch the full array back.

The same applies to VarString and VarBytes fields: a PATCH replaces the entire value.