Chalk home page
  1. Resolvers
  2. Inputs

Resolvers can depend on other features to compute their features. These dependencies are declared through the type signature of the arguments to the resolver function.

Scalar dependencies

To depend on a feature from a Feature Set, you give your resolver an argument with that feature as the type. You can then use that argument in the body of your resolver to compute your output features. If you’re running our editor plugin, your editor will see the type of each variable as the type of the underlying scalar.

def fn(a: -> ...
    # type(email) == str

You can require multiple features in a resolver. However, all feature dependencies in a single resolver need to originate at the same root namespace:

Requiring features from the same root namespace

def fn(a:, b: -> ...

Here, we incorrectly request features from the root namespaces of Transfer and User:

Requiring features from different root namespaces

def fn(a:, b: Transfer.amount) -> ...

Has-one dependencies

Scalar has-one

You can also require features joined to a Feature Set through has-one relationships. For example, if users in your system have bank accounts, and you wanted to compare the name on the user’s bank account to the user’s name, you could require the user’s name and the account’s title through the user:

def name_sim(title: User.account.title, name: User.full_name) -> ...

You can also require all scalars on the user’s profile:

def fn(profile: User.profile) -> ...:

Chalk will materialize all scalar features on the profile before calling this function. If you want to pull only a few features from the profile, require each directly:

def fn(signup_date: User.profile.signup_date, age: User.profile.age) -> ...

Optional has-one

Has-one relationships can also be declared as optional. You may also require feature through optional relationships, but the types for all of those optional features will become optional. Consider the below example:

class Account:
    balance: float  # Non-optional balance

class User:
    account: Optional[Account] = has_one(...)  # Optional relationship

def fn(balance: User.account.balance) -> ...:
    # Balance will be "float | None"

The resolver in this example receives an optional float, even though balance is not an optional field on Account. The optional is added because the user may not have an account, in which case the resolver will receive None for the balance.

Nested has-one

You can also traverse nested has-one relationships in the same manner as requiring a single has-one.

Consider a schema where users have a feature set of profile information, and the user’s profile has an identity feature set, which in turn has the age of the user’s email. You can require the email age feature as below:

def fn(email_age: User.profile.identity.email_age) -> ...

However, you cannot access nested relationships without explicit asking for them.

Accessing a transitive relationship from a dependency.

def fn(acct: User.account) -> ...:
    acct.balance           # Ok  # Error!

Instead, you can require the nested relationship directly and access any of its scalar features.

Directly requiring the transitive relationship.

def fn(ins: User.account.institution, acct: User.account) -> ...:
    acct.balance  # Ok      # Ok

The semantics of optional has-one dependencies carry over to nested has-one dependencies. If you traverse an optional relationship, then all downstream attributes will become optional.

Has-many dependencies

Scalar has-many

You can also require has-many relationships as inputs to your resolver:

def fn(transfers: User.transfers) -> ...:

You receive a Chalk DataFrame, which supports projections, filtering, and aggregations, among other operations.

Limiting data fetching

By default, Chalk will materialize all scalar features on the Transfer feature set before calling your resolver. As an optimization hint, you can specify which features from the transfers that you’d like Chalk to materialize before calling the function. For example, if there were expensive features to compute on the transfer, you could scope the features to only the set you need:

def fn(transfers: User.transfers[Transfer.amount, Transfer.memo]) -> ...:
    transfers[Transfer.amount].sum()      # Ok
    transfers[Transfer.from_institution]  # Error: filtered out above

The error above is surfaced statically by our editor plugin.


You can apply filters to the has-many inputs of resolvers:

def fn(transfers: User.transfers[Transfer.amount > 100]) -> ...:

Filters can be composed with projections following the semantics of the Chalk DataFrame.

def fn(transfers: User.transfers[Transfer.amount > 100, Transfer.memo]) -> ...:

Has-many through has-one

Has-many relationships can be required through has-one relationships:

def fn(transfers: User.account.transfers) -> ...:

As with scalar has-many dependencies, you can scope down the scalar features on the transfer to only those required:

def fn(transfers: User.account.transfers[Transfer.amount]) -> ...:
    transfers[Transfer.amount].sum()      # Ok
    transfers[Transfer.from_institution]  # Error: filtered out above

Has-many through optional-has-one

If the has-one relationship that you’re traversing is optional, then the transfers argument in the example above will either be None or a Chalk DataFrame.

Has-one through has-many

def fn(ts: User.transfers[Transfer.account, Transfer.amount]) -> ...:
    ts[Transfer.account.balance].sum()  # Ok
    ts[Transfer.amount].sum()           # Ok
    ts[Transfer.memo]                   # Error! Filtered out

You can also refine the types pulled from the nested has-one:

def fn(ts: User.transfers[Transfer.account.balance]) -> ...:
    ts[Transfer.account.balance].sum()  # Ok
    ts[Transfer.account.title]          # Error! Filtered out