Chalk home page
Docs
SDK
CLI
  1. Features
  2. Has One

Has-one relationships link a feature to a single instance of another feature.

The simplest way to specify a join for a has-one relationship is implicitly. In the example below, a User is linked to their Profile.

from chalk.features import features

@features
class Profile:
    id: str
    user_id: "User.id"
    email_age_years: float

@features
class User:
    id: str
    profile: Profile

With a has-one relationship established, you can reference features on Profile through User. For example:

user_email_age = User.profile.email_age_years

Explicit Join

In the following snippet, the has-one join is explicitly defined. This is functionally equivalent to the recommended implementation:

from chalk.features import features, has_one, ...

@features
class Profile:
    id: str
    user_id: str
    email_age_years: float

@features
class User:
    id: str
    uid: str
    profile: Profile = has_one(lambda: Profile.user_id == User.uid)

The lambda solves forward references, letting you reference User before it is defined.

Composite Join Keys

You can also specify a composite join key for a has-one relationship. For example, if a User is linked to a Profile by org and email, you can define the join as follows:

from chalk.features import features, has_one
from datetime import datetime

@features
class User:
    id: str
    email: str = _.alias + "-" + _.org + _.domain
    org_domain: str = _.org + _.domain
    org: str
    domain: str
    alias: str

    # join with composite key
    posts: DataFrame[Posts] = has_many(lambda: User.email == Post.email)
    # multi-feature join
    org_profile: Profile = has_one(lambda: (User.alias == Profile.email) & (User.org == Profile.org))

@features
class Workspace:
    id: str
    # join with child-class's composite key
    users: DataFrame[Users] = has_many(lambda: Workspace.id == User.org_domain)

Back-references

One-to-one

You can also add a back-reference to User from Profile. However, you don’t have to explicitly set the join on Profile. Instead, the join condition is assumed to be symmetric and copied over. To complete the one-to-one relationship from our example, add a User to the Profile class:

@features
class Profile:
  ...
  user_id: "User.uid"
  email_age_years: float
  user: "User"

@features
class User:
  ...
  uid: str
  profile: Profile

Here you need to use quotes around `User` to use a forward reference.

Optional relationships

When a has-one relationship is specified, the default behavior is to treat the linked Feature as required. Following the example above, specifying a User without a Profile and querying for a User’s profile or using the User.profile in a resolver raises an error.

To define optional relationships, use the typing.Optional[...] keyword:

from typing import Optional

@features
class User:
  ...
  uid: Profile.user_id
  profile: Profile
  profile: Optional[Profile]

Note, resolvers that take optional features as inputs need to handle the None case. This is covered in more detail in the resolver’s section of the docs.


Chained Has-One Joins

You can chain has-one joins to traverse multiple relationships. For example, you could define the following features to represent a user’s profile and preferences in an application.

from chalk.features import features, Primary

@features
class User:
    id: str
    email: str

@features
class Profile:
    id: Primary[User.id]
    username: str

@features
class Preferences:
    id: Primary[Profile.id]
    dark_mode: bool


Organizing Feature Definitions Across Files

In larger projects, it’s common to split feature definitions across multiple Python modules. For unidirectional dependencies, this is straightforward. For example, if user.py imports profile.py, you can define the User and Profile features in separate files without issues. However, if you have circular dependencies, you may run into problems.

Chalk supports this, but circular imports can arise when features reference each other across files. To avoid these issues, use the if TYPE_CHECKING block from the typing module and quote your forward references.

Here’s an example of how to do this cleanly:

src/profile.py
# Imports User directly, because user.py
# wont import profile.py
from src.user import User

@features
class Profile:
    id: Primary[User.id]
    username: str
src/user.py
from chalk.features import features
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    # Imports Profile only when type checking
    # to avoid circular imports
    from src.profile import Profile

@features
class User:
    id: str
    # Profile must be quoted because it is imported
    # only when type checking
    profile: "Profile"

By quoting imports inside if TYPE_CHECKING, you avoid circular dependency errors while still maintaining type safety and feature linkage.


Querying for Has-One Relationships

You can also query for a feature that is joined through a has-one relationship by referencing the root namespace. For example, to query for the Profile features associated with a User continuing from the example above, you can write:

from chalk.client import ChalkClient
from src.features import User

client =  ChalkClient()
client.query(
    input={User.id: "1"},
    output=[User.profile.id, User.profile.email_age_years]
)