Entities and mapping

A Canyon entity starts as an ordinary Rust struct. Its fields tell the mapper what to read from a row. The database table still has to exist, and its columns must match the model.

For the teams table from the previous chapter:


#![allow(unused)]
fn main() {
use canyon_sql::macros::{canyon_entity, CanyonMapper, Crud, Fields};

#[derive(Debug, Fields, Crud, CanyonMapper)]
#[canyon_entity(table_name = "teams")]
pub struct Team {
    #[primary_key]
    pub id: i64,
    pub name: String,
}
}

There are four Canyon pieces in this example:

  • #[canyon_entity] supplies the table name and processes field markers such as #[primary_key]. It also registers this struct for the experimental migrations machinery.
  • CanyonMapper converts driver rows to Team.
  • Crud generates read, insert, update, and delete operations.
  • Fields generates names and typed values for the query builder. Ordinary find_all() or insert() calls do not need it.

When do you need #[canyon_entity]?

The derives can infer team from Team without an entity attribute. If a model has no Canyon field markers and its table follows that naming convention, you can omit #[canyon_entity].

Use #[canyon_entity] when:

  • The physical table or schema differs from the inferred name: pass table_name = "..." or schema = "...".
  • You annotate fields with Canyon markers such as #[primary_key] or #[foreign_key(...)]. The current attribute macro processes these markers; keep it on models that use them.

Registration for experimental migrations is a side effect, not a reason to enable that feature for normal CRUD. Query execution uses the metadata generated by the derives; it does not look up a runtime registry of entities.

Table names and schemas

Without an explicit name, Canyon derives a snake_case table name from the Rust type: TournamentDetails maps to tournament_details. Existing schemas do not always follow that rule. Use explicit metadata when they do not:


#![allow(unused)]
fn main() {
#[canyon_entity(table_name = "tournament_entries", schema = "public")]
}

Generated CRUD and relationship operations on the model use that name. Repository adapters have a current limitation with custom table names.

Primary keys

#[primary_key] identifies the field used by key-based reads, updates, and deletes. For a numeric key, it is treated as database-generated and auto-incrementing unless you say otherwise. After a successful insert(&mut self), Canyon writes the generated key back to the Rust instance.


#![allow(unused)]
fn main() {
#[primary_key(autoincremental = false)]
pub external_id: i64,
}

Use the non-incrementing form when your application supplies the key. You can map a table without annotating a primary key, but find_by_pk, update, and delete then have no key to use and return a typed error.

What Fields generates

For Team, the derive exposes three types:

  • TeamTable represents table metadata.
  • TeamField names a column, for example TeamField::name. Joins and ordering need this form.
  • TeamFieldValue pairs a column with a value of its Rust type, for example TeamFieldValue::name("Blue".to_owned()). Predicates need both pieces.

Fields is the supported API in 0.5.1, and several models may derive it in one Rust module. A future major version may use typed column descriptors instead. Nothing is being removed in this release.

The query-builder chapter shows how the enums work now.

Mapping failures are real errors

A row may contain extra columns; the derived mapper ignores those it does not need. It cannot ignore a problem with a field the struct does require:

  • A required column is missing.
  • A non-optional field receives SQL NULL.
  • The database value cannot be converted to the Rust field type.

Each case returns CanyonError::Mapping, not an empty result.

Nullable column? Use Option<T> on the model field when the SQL column can contain NULL.

The CRUD chapters reuse this Team model when they omit its definition.

For an unusual projection, you can implement canyon_sql::core::RowMapper yourself. Its backend-specific deserialization methods return CanyonResult. You then take responsibility for column names, nullability, and conversions on every backend you enable; start with CanyonMapper when a normal row struct will do.