does kitesql support nested queries, for example:
```rs
struct Person
{
name: Name,
age: u32,
}
struct Name
{
first: String,
last: String,
}
fn main()
{
let name = Person{Name: {first: "john", last: "Doe"}, age: 25};
}
```
where I want to query the person's first and last name?
@Raj2032 Thanks for the question. A nested field could represent either an association between two models or an embedded value stored in the same row, and KiteSQL ORM does not currently infer either mapping automatically.
If `Name` is an associated model, the relationship can be represented explicitly with a scalar key. The application-level nested field can be excluded from ORM persistence with `#[model(skip)]`:
```rust
#[derive(Default, Debug, PartialEq, Model)]
#[model(table = "names")]
struct Name {
#[model(primary_key)]
id: u32,
first: String,
last: String,
}
#[derive(Default, Debug, PartialEq, Model)]
#[model(table = "persons")]
struct Person {
#[model(primary_key)]
id: u32,
name_id: u32,
#[model(skip)]
name: Option<Name>,
age: u32,
}
```
The related value can then be loaded explicitly using `name_id`, either with another bound query, an explicit join, or a batched query, and assigned to `name` by the application:
```rust
if let Some(mut person) = database.get::<Person>(&person_id)? {
person.name = database.get::<Name>(&person.name_id)?;
}
```
`#[model(skip)]` fields are not included in schema generation, inserts, or updates, and are initialized with `Default::default()` when a row is decoded. This pattern does not require a database-level foreign-key constraint; the scalar ID can remain an application-managed reference.
For collections of people, an explicit join or a batched query should be used instead of loading each related value separately, to avoid N+1 queries. Automatic relationship mapping and eager/lazy loading are currently low-priority ORM features.
KiteSQL ORM currently aims to keep relationship handling lightweight and explicit. If you have a concrete usage pattern in mind, we would be happy to discuss a better API shape and improve this workflow without introducing heavyweight relationship machinery.
Hi @KKould thanks for your response :) So I was kinda more looking for an approach where you can do nested queries without having to do additional IDs, for example in native_db it is possible:
https://github.com/vincent-herlemont/native_db/issues/205
I get that native_db is not SQL it is quite different but I was wondering if by any chance something similar could be implemented or if there is another way similar to this link?
Thanks, I took a closer look at the `native_db` example, and I now have a much clearer understanding of the model you are looking for.
I updated the example using a similar application-level pattern:
```rust
use kite_sql::db::{DataBaseBuilder, Database, ResultIter};
use kite_sql::errors::DatabaseError;
use kite_sql::storage::Storage;
use kite_sql::Model;
// cargo run --example orm_explicit_relationship --no-default-features --features orm,rocksdb
#[derive(Debug, PartialEq)]
struct Name {
first: String,
last: String,
}
#[derive(Debug, PartialEq)]
struct Person {
id: u32,
name: Name,
age: u32,
}
// The SQL representation remains private to the persistence layer. Both name
// fields belong to the persons table; Name is not a separate model or table.
#[derive(Debug, PartialEq, Model)]
#[model(table = "persons")]
struct PersonRecord {
#[model(primary_key)]
id: u32,
#[model(index)]
name_first: String,
name_last: String,
age: u32,
}
impl From<&Person> for PersonRecord {
fn from(person: &Person) -> Self {
Self {
id: person.id,
name_first: person.name.first.clone(),
name_last: person.name.last.clone(),
age: person.age,
}
}
}
impl From<PersonRecord> for Person {
fn from(record: PersonRecord) -> Self {
Self {
id: record.id,
name: Name {
first: record.name_first,
last: record.name_last,
},
age: record.age,
}
}
}
impl Person {
fn create_table<S: Storage>(database: &mut Database<S>) -> Result<(), DatabaseError> {
database.create_table::<PersonRecord>()
}
fn insert<S: Storage>(&self, database: &Database<S>) -> Result<(), DatabaseError> {
database.insert(&PersonRecord::from(self))
}
fn load<S: Storage>(database: &Database<S>, id: &u32) -> Result<Option<Self>, DatabaseError> {
Ok(database.get::<PersonRecord>(id)?.map(Into::into))
}
fn find_by_first_name<'db, S: Storage>(
database: &'db Database<S>,
first_name: &str,
) -> Result<impl Iterator<Item = Result<Self, DatabaseError>> + 'db, DatabaseError> {
Ok(database
.bind(|ctx| {
ctx.from::<PersonRecord>()?
.filter(|e| e.column(PersonRecord::name_first())?.eq(first_name))?
.finish()
})?
.orm::<PersonRecord>()
.map(|record| record.map(Into::into)))
}
}
fn main() -> Result<(), DatabaseError> {
let data_dir = tempfile::tempdir().expect("create a temporary RocksDB directory");
let mut database = DataBaseBuilder::path(data_dir.path()).build_rocksdb()?;
Person::create_table(&mut database)?;
for person in [
Person {
id: 1,
name: Name {
first: "John".to_string(),
last: "Doe".to_string(),
},
age: 25,
},
Person {
id: 2,
name: Name {
first: "Jane".to_string(),
last: "Doe".to_string(),
},
age: 30,
},
] {
person.insert(&database)?;
}
let person = Person::load(&database, &1)?.unwrap();
println!("loaded: {person:?}");
for person in Person::find_by_first_name(&database, "John")? {
println!("queried by first name: {:?}", person?);
}
Ok(())
}
```
In this example, `Person` and `Name` are treated as one aggregate. `Name` does not have its own table or ID. Its fields are stored as `name_first` and `name_last` in the `persons` table, and the persistence layer reconstructs them into the nested `Name` struct when reading rows. It also demonstrates querying by the first name while still returning a nested `Person`.
Whether `Name` should use a separate table depends on its semantics. If it has its own identity, is shared by multiple records, or needs to be managed independently, a separate relational model is usually easier to maintain. However, if `Name` is simply a value owned by `Person`, storing its fields in the same table is also a common and reasonable SQL design.
A separate table would introduce additional IDs, joins, and relationship-management costs. If you do not need that independent lifecycle, the pattern shown in the updated example should satisfy your use case while keeping the public Rust model nested.
KiteSQL ORM does not currently flatten nested fields automatically, so the example uses a private persistence record to map the SQL columns back into the nested Rust structure. This also gives us a concrete pattern for discussing a more ergonomic embedded-field API in the future.
Hi @KKould , thanks again for your response.
Regarding this code:
```rs
// The SQL representation remains private to the persistence layer. Both name
// fields belong to the persons table; Name is not a separate model or table.
#[derive(Debug, PartialEq, Model)]
#[model(table = "persons")]
struct PersonRecord {
#[model(primary_key)]
id: u32,
#[model(index)]
name_first: String,
name_last: String,
age: u32,
}
impl From<&Person> for PersonRecord {
fn from(person: &Person) -> Self {
Self {
id: person.id,
name_first: person.name.first.clone(),
name_last: person.name.last.clone(),
age: person.age,
}
}
}
impl From<PersonRecord> for Person {
fn from(record: PersonRecord) -> Self {
Self {
id: record.id,
name: Name {
first: record.name_first,
last: record.name_last,
},
age: record.age,
}
}
}
impl Person {
fn create_table<S: Storage>(database: &mut Database<S>) -> Result<(), DatabaseError> {
database.create_table::<PersonRecord>()
}
fn insert<S: Storage>(&self, database: &Database<S>) -> Result<(), DatabaseError> {
database.insert(&PersonRecord::from(self))
}
fn load<S: Storage>(database: &Database<S>, id: &u32) -> Result<Option<Self>, DatabaseError> {
Ok(database.get::<PersonRecord>(id)?.map(Into::into))
}
fn find_by_first_name<'db, S: Storage>(
database: &'db Database<S>,
first_name: &str,
) -> Result<impl Iterator<Item = Result<Self, DatabaseError>> + 'db, DatabaseError> {
Ok(database
.bind(|ctx| {
ctx.from::<PersonRecord>()?
.filter(|e| e.column(PersonRecord::name_first())?.eq(first_name))?
.finish()
})?
.orm::<PersonRecord>()
.map(|record| record.map(Into::into)))
}
}
```
This seems to be closer to `native_db` but as you mentioned hopefully in the near future there would be a more ergonomic way of doing it as it is quite long winded code (similarly to how `native_db` does it). Maybe via macros or something but anyways something to keep in mind thanks anyways :)