Skip to content

Model

relationalai.semantics.frontend.base
Model(
name: str = "",
exclude_core: bool = False,
is_library: bool = False,
config: Config | None = None,
)

Create and manage a semantic model.

A Model stores the concepts, relationships, definitions, and requirements that make up your semantic model. Create them using methods like Model.Concept, Model.Relationship, Model.define, and Model.require. Query methods like Model.where and Model.select return a composable Fragment that you can further refine and materialize with Fragment.to_df.

Many methods on Model can be accessed as top-level functions from the relationalai.semantics module. For example, you can use define instead of Model.define to add definitions to a model if you have a single active model (the common case in interactive use). However, if you have multiple models or want to be explicit about which model you’re using, it’s best to call methods directly on the model instance.

  • name

    (str, default: "") - Name of the model.
  • exclude_core

    (bool, default: False) - If True, do not include the core library by default.
  • is_library

    (bool, default: False) - If True, this model is treated as a library and is not added to the global list of active models.
  • config

    (Config, default: None) - Configuration used when executing queries. Defaults to None which means configuration is loaded from the filesystem (for example, from a raconfig.yaml file if present).

Define a few entities and query them:

>>> from relationalai.semantics import Model
>>> m = Model()
>>> Person = m.Concept("Person")
>>> m.define(
... Person.new(name="Alice", age=10),
... Person.new(name="Bob", age=30),
... )
>>> m.where(Person.age > 18).select(Person.name).to_df()
Model.all_models: list[Model]

Class-level list of models created in this Python session (excluding library-only models). Most users do not need this.

Model.name: str

The model name (optional; used for identification/debugging).

Model.config: relationalai.config.config.Config

Configuration settings used when executing queries against this model. If you have a raconfig.yaml file, or other valid configuration source, these settings are automatically loaded into new models by default. You can also override them by passing a custom config when creating the model. See the relationalai.config documentation for more details.

Model.defines: KeyedSet[Fragment]

Definitions added via Model.define.

Model.requires: KeyedSet[Fragment]

Requirements added via Model.require.

Model.exports: KeyedSet[Fragment]

Export artifacts created when exporting query results (advanced).

Model.libraries: list[type[Model]]

Libraries included with this model (the core library is included by default unless you set exclude_core=True).

Model.concepts: list[Concept]

Concepts created in this model via Model.Concept.

Model.tables: list[Table]

Table references created in this model via Model.Table.

Model.relationships: list[Relationship]

Relationships and properties created in this model via Model.Relationship.

Model.enums: list[type[ModelEnum]]

Enum types created in this model via Model.Enum.

Model.concept_index: dict[str, Concept]

Dictionary mapping concept names to Concept objects for quick lookup.

Model.table_index: dict[str, Table]

Dictionary mapping table names to Table objects for quick lookup.

Model.relationship_index: dict[str, Relationship]

Dictionary mapping relationship/property names to Relationship objects for quick lookup.

Model.enums_index: dict[str, type[ModelEnum]]

Dictionary mapping enum names to enum types for quick lookup.

Model.Enum: type

Helper used to create enums associated with this model.

Model.Concept(
name: str,
extends: list[Concept] = [],
identify_by: dict[str, Property | Concept] = {},
identity_includes_type: bool = False,
) -> Concept

Create a concept type in this model.

This is the primary way to introduce a new Concept (entity type) into a Model.

Parameters:

  • name

    (str) - Name of the concept.

  • extends

    (list[Concept], default: []) - Base concepts this concept extends. Relationships declared on base concepts are inherited by (and available on) the derived concept.

  • identify_by

    (dict[str, Property | Concept], default: {}) - Identity fields for this concept. The keys are identity attribute names and the values specify their types (most commonly a Concept such as semantics.frontend.core.Integer or semantics.frontend.core.String).

    Identity keys are used by Concept.new to refer to an entity by key (find-or-create).

  • identity_includes_type

    (bool, default: False) - If True, the subtype is part of an entity’s key. Example: when A and B share a base identified by id, A.new(id=1) and B.new(id=1) create two different entities. If False, both calls refer to the same entity (same key fields), which can cause collisions across a type hierarchy.

Returns:

  • Concept - The newly created concept.

Raises:

  • relationalai.util.error.RAIException - If extends contains non-Concept values, or if identify_by contains values that are not Concepts or Properties.

Examples:

Create a concept and define a couple of entities:

>>> from relationalai.semantics import Model, String
>>> m = Model()
>>> Person = m.Concept("Person", identify_by={"name": String})
>>> m.define(
... Person.new(name="Alice"),
... Person.new(name="Bob"),
... )

Create a derived concept:

>>> Adult = m.Concept("Adult", extends=[Person])
>>> m.where(Person.age >= 18).define(Adult(Person))

Define an identity key:

>>> from relationalai.semantics import Integer
>>> Item = m.Concept("Item", identify_by={"id": Integer})
>>> m.define(Item.new(id=1))

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Build a semantic model
        ├──  Declare concepts
        │   ├──  Declare a concept with Model.Concept
        │   ├──  Declare a concept with an identity scheme
        │   └──  Declare a subconcept with extends
        └──  Derive facts with logic
            ├──  Write conditional definitions with Model.where and Model.define
            └──  Match multiple entities of the same type with Concept.ref
Model.Table(path: str, schema: dict[str, Concept] = {}) -> Table

Create a table reference in this model.

A Table represents an external table name (such as a SQL table) that you can use as a source of schema/data for defining entities, or as a target for exports via Fragment.into.

Parameters:

  • path

    (str) - Table name or path (for example "MY_DB.MY_SCHEMA.CUSTOMERS").
  • schema

    (dict[str, Concept], default: {}) - Optional mapping of column names to their types.

Returns:

  • Table - The created table reference.

Examples:

Create a table reference for use as an export target:

>>> from relationalai.semantics import Integer, Model, String
>>> m = Model()
>>> Person = m.Concept("Person", identify_by={"id": Integer})
>>> Person.name = m.Property(f"{Person} is named {String:name}")
>>> m.define(Person.new(id=1, name="Alice"))
>>> out = m.Table("DB.SCHEMA.PERSONS_EXPORT")
>>> m.select(Person.id, Person.name).into(out).exec()

Use a table with a known schema as an input when defining entities:

>>> from relationalai.semantics import Model, Integer, String
>>> m = Model()
>>> source = m.Table("DB.SCHEMA.CUSTOMERS", schema={"id": Integer, "name": String})
>>> Customer = m.Concept("Customer", extends=[Person])
>>> m.define(Customer.new(source.to_schema()))

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Build a semantic model
        ├──  Declare data sources
        │   ├──  Choose the right source type
        │   └──  Use a Snowflake table with Model.Table
        ├──  Define base facts
        └──  Query a model
            └──  Export results to Snowflake tables with into and exec
Model.Relationship(
reading: str = "", fields: list[Field] = [], short_name: str = ""
) -> Relationship

Create a relationship in this model.

This is the primary way to introduce a new Relationship type into a Model. The most common way to define a relationship is with a Python f-string reading that embeds concept/type placeholders. Each embedded placeholder becomes a field of the relationship, in order.

Reading strings have the following format:

  • Use an f-string and embed types with {...}, e.g. f"{Person} has {Pet}".
  • Name fields with {Type:name}, e.g. f"{Person:owner} has {Pet:pet}".
  • For full control (including multi-field relationships), pass an explicit fields=[Field(...), ...] list instead of a reading.

Parameters:

  • reading

    (str, default: "") - A reading that describes the relationship fields. Typically an f-string, for example f"{Person} has {Pet}" or f"{Integer:i} has {String:name}".
  • fields

    (list[Field], default: []) - Explicit fields for the relationship. If provided and reading is empty, a default reading is generated from the fields.
  • short_name

    (str, default: "") - Optional identifier used as the key in model.relationship_index.

Returns:

Raises:

  • ValueError - If neither reading nor fields is provided.

Examples:

Create a relationship between two concepts and define one fact:

>>> from relationalai.semantics import Model, String, define, where
>>> m = Model()
>>> Person = m.Concept("Person", identify_by={"name": String})
>>> Pet = m.Concept("Pet", identify_by={"name": String})
>>> Person.name = m.Property(f"{Person} has {String:name}")
>>> Person.pets = m.Relationship(f"{Person} has {Pet}")
>>> define(
... alice := Person.new(name="Alice"),
... boots := Pet.new(),
... alice.pets(boots),
... )
>>> where(Person.pets).select(Person.name).to_df()

Create a relationship by specifying fields directly and give it a stable name via short_name:

>>> from relationalai.semantics import Model, String, define
>>> from relationalai.semantics.frontend.base import Field
>>> m = Model()
>>> Person = m.Concept("Person", identify_by={"name": String})
>>> has_pet = m.Relationship(
... fields=[Field("person", Person), Field("pet_name", String)],
... short_name="has_pet",
... )
>>> define(alice := Person.new(name="Alice"), has_pet(alice, "boots"))
>>> has_pet.to_df()

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Build a semantic model
        └──  Declare relationships and properties
            └──  Declare a relationship with Model.Relationship
Model.Property(
reading: str = "", fields: list[Field] = [], short_name: str = ""
) -> Property

Create a property in this model.

This is the primary way to introduce a new Property into a Model. The most common way to define a property is with a Python f-string reading that embeds concept/type placeholders. Each embedded placeholder becomes a field of the property, in order.

Reading strings have the following format:

  • Use an f-string and embed types with {...}, e.g. f"{Person} has {Integer:age}".
  • Name fields with {Type:name} (the name becomes the attribute you access, e.g. Person.age).
  • For full control, pass an explicit fields=[Field(...), ...] list.

Parameters:

  • reading

    (str, default: "") - A reading that describes the property fields. Typically an f-string, for example f"{Person} has {Integer:age}".
  • fields

    (list[Field], default: []) - Explicit fields for the property. If provided and reading is empty, a default reading is generated from the fields.
  • short_name

    (str, default: "") - Optional identifier used as the key in model.relationship_index.

Returns:

Raises:

  • ValueError - If neither reading nor fields is provided.

Examples:

Declare a property on a concept, define a couple of entities, and query:

>>> from relationalai.semantics import Integer, Model, String
>>> m = Model()
>>> Person = m.Concept("Person", identify_by={"name": String})
>>> Person.age = m.Property(f"{Person} has {Integer:age}")
>>> m.define(Person.new(name="Alice", age=10), Person.new(name="Bob", age=30))
>>> m.where(Person.age >= 18).select(Person.age).to_df()

Create a property by specifying fields directly and give it a stable name via short_name:

>>> from relationalai.semantics import Integer, Model, String
>>> from relationalai.semantics.frontend.base import Field
>>> m = Model()
>>> Person = m.Concept("Person", identify_by={"name": String})
>>> age = m.Property(
... fields=[Field("person", Person), Field("age", Integer)],
... short_name="age",
... )
>>> Person.age = age

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Build a semantic model
        ├──  Declare concepts
        └──  Declare relationships and properties
            └──  Declare a property with Model.Property
Model.select(*args: StatementAndSchema) -> Fragment

Return a selection fragment in this model.

This starts a new Fragment with a select clause. The returned fragment is composable and can be chained with Fragment.where, Fragment.define, and Fragment.require, then materialized via Fragment.to_df.

You can select most values and expressions, including:

  • Concepts, relationships/properties, and fields
  • Variables and expressions (including arithmetic and comparisons)
  • Aggregates and grouped expressions (for example per(x).sum(y))
  • Python literals like strings, integers, floats, and booleans

Passing a TableSchema (typically created via Table.to_schema) expands to selecting each column of the table. Selecting a comparison expression like Person.age >= 18 produces a boolean output column.

Parameters:

Returns:

  • Fragment - A composable query fragment representing the selection.

Examples:

Select a value and a computed boolean column:

>>> from relationalai.semantics import Integer, Model, String
>>> m = Model()
>>> Person = m.Concept("Person", identify_by={"name": String})
>>> Person.age = m.Property(f"{Person} has {Integer:age}")
>>> m.define(Person.new(name="Alice", age=10), Person.new(name="Bob", age=30))
>>> m.select(Person.name, Person.age >= 18).to_df()

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Build a semantic model
        ├──  Declare data sources
        │   ├──  Use a Snowflake table with Model.Table
        │   ├──  Use a DataFrame with Model.data
        │   ├──  Use inline Python data with Model.data
        │   └──  Create model constants with Model.Enum
        ├──  Derive facts with logic
        │   └──  Understand PyRel logic constructs
        └──  Query a model
            ├──  Understand fragments and materialization
            └──  Select values with select
Model.where(*args: Statement) -> Fragment

Return a fragment filtered by the given conditions.

This starts a new Fragment with a where clause. The returned fragment is composable and can be chained with Fragment.select, Fragment.define, and Fragment.require, then materialized via Fragment.to_df.

Multiple conditions passed in a single call are combined with AND. You can add additional filters by chaining Fragment.where.

Parameters:

Returns:

  • Fragment - A composable query fragment representing the filter(s).

Examples:

Filter and select:

>>> from relationalai.semantics import Integer, Model, String
>>> m = Model()
>>> Person = m.Concept("Person", identify_by={"name": String})
>>> Person.age = m.Property(f"{Person} has {Integer:age}")
>>> m.define(Person.new(name="Alice", age=10), Person.new(name="Bob", age=30))
>>> m.where(Person.age > 21).select(Person.name).to_df()

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Build a semantic model
        ├──  Derive facts with logic
        │   ├──  Understand PyRel logic constructs
        │   └──  Write conditional definitions with Model.where and Model.define
        ├──  Define requirements
        │   └──  Add a scoped requirement with Model.where
        └──  Query a model
            └──  Filter results with where
Model.require(*args: Statement) -> Fragment

Return a fragment that adds requirements (constraints).

This starts a new Fragment with a require clause. The returned fragment is composable and can be chained with Fragment.where to make a requirement conditional, or with Fragment.require to add additional requirements.

A requirement is an existence check: it fails when there are no matches. Roughly speaking, m.require(expr) asserts EXISTS(expr) and fails when NOT EXISTS(expr).

For example, m.require(Person.age > 0) fails in the following cases:

  • There are no Person entities.
  • There are Person entities, but none have an age value.
  • There are age values, but none are greater than zero.

It does not fail just because some Person entities are missing an age value, or because some ages are non-positive, as long as there is at least one match for Person.age > 0.

To enforce a per-entity requirement, scope it with Model.where (for example, where(Person).require(Person.age > 0)) or use Concept.require (for example, Person.require(Person.age > 0)). Both of those forms are equivalent and the choice is mostly a matter of style.

Multiple requirements passed in a single call are enforced together. You can also apply requirements to an existing fragment by calling Fragment.require.

Parameters:

Returns:

  • Fragment - A composable fragment representing the requirement(s).

Examples:

Require that at least one match exists:

>>> from relationalai.semantics import Integer, Model, String
>>> m = Model()
>>> Person = m.Concept("Person", identify_by={"name": String})
>>> Person.age = m.Property(f"{Person} has {Integer:age}")
>>> m.define(Person.new(name="Alice", age=1), Person.new(name="Bob", age=2))
>>> # Passes because there exists a Person with age >= 0.
>>> m.require(Person.age >= 0)

Scope a requirement to enforce it per entity:

>>> # Requires every Person to have an age >= 0.
>>> m.where(Person).require(Person.age >= 0)

Notes:

Requirements can affect performance:

  • Adding more requirements generally increases compilation and execution work, especially if a requirement introduces large joins, broad disjunctions (for example via Model.union), or aggregates over large domains.
  • A well-chosen requirement can also improve performance by narrowing what needs to be considered (for example by reducing the number of matches that flow into later joins or aggregates).
  • Prefer scoped requirements (for example m.require(...).where(...)) when the constraint should only apply under certain conditions.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Build a semantic model
        └──  Define requirements
            ├──  How a requirement works
            └──  Add a global requirement with Model.require
Model.define(*args: Statement) -> Fragment

Return a fragment that adds definitions (facts or logic).

This starts a new Fragment with a define clause. The returned fragment is composable and can be chained with Fragment.where to make definitions conditional, or with Fragment.define to add additional definitions.

Calling Model.define also registers the resulting definition fragment with the model, so the definitions are included when the model is compiled and evaluated by later queries.

Parameters:

Returns:

  • Fragment - A composable fragment representing the definition(s).

Examples:

Define some base facts:

>>> from relationalai.semantics import Integer, Model, String
>>> m = Model()
>>> Person = m.Concept("Person", identify_by={"name": String})
>>> Person.age = m.Property(f"{Person} has {Integer:age}")
>>> m.define(Person.new(name="Alice", age=10), Person.new(name="Bob", age=30))

Define a derived concept under a condition:

>>> Adult = m.Concept("Adult", extends=[Person])
>>> m.where(Person.age >= 18).define(Adult(Person))

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Build a semantic model
        ├──  Declare concepts
        │   └──  Declare a concept with Model.Concept
        ├──  Define base facts
        │   ├──  Define entities and their properties with Concept.new
        │   └──  Define relationship facts
        └──  Derive facts with logic
            ├──  Understand PyRel logic constructs
            ├──  Write conditional definitions with Model.where and Model.define
            └──  Compute derived property values
Model.union(*items: Value) -> Union

Combine items with a logical OR or a set-style union.

This constructs a Union object that represents a disjunction over multiple branches.

Unlike the | operator (see Variable.__or__), which picks the first branch that can succeed, union combines the results from all branches.

union is commonly used in two ways:

  • As an OR-combinator in Model.where clauses, for example m.where(m.union(cond1, cond2)).
  • As a union of multiple derived tables/fragments that return the same number of values. The resulting union behaves like a derived table whose columns can be selected or unpacked.

Nested unions are flattened, so m.union(m.union(a, b), c) is equivalent to m.union(a, b, c).

Parameters:

  • *items

    (Value, default: ()) - One or more semantics.frontend.base.Value items to union. Each branch must return the same number of values. Branches used purely as filters (for example comparisons, or Not) contribute zero output values and are intended to be used as conditions in a where clause.

Returns:

  • Union - A composable union object.

Raises:

  • relationalai.util.error.RAIException - Raised if the union branches return different numbers of values.

Examples:

Use union to OR conditions in a where clause:

>>> from relationalai.semantics import Integer, Model, String
>>> m = Model()
>>> Person = m.Concept("Person", identify_by={"name": String})
>>> Person.age = m.Property(f"{Person} has {Integer:age}")
>>> m.define(Person.new(name="Alice", age=10), Person.new(name="Bob", age=70))
>>> m.where(m.union(Person.age < 18, Person.age >= 65)).select(Person.name).to_df()

Union two fragments that select the same number of values:

>>> Edge = m.Concept("Edge")
>>> Edge.src = m.Property(f"{Edge} has {Integer:src}")
>>> Edge.dst = m.Property(f"{Edge} has {Integer:dst}")
>>> m.define(Edge.new(src=1, dst=2), Edge.new(src=4, dst=3))
>>> e, src, dst = m.union(
... m.where(Edge, Edge.src <= Edge.dst).select(Edge, Edge.src, Edge.dst),
... m.where(Edge, Edge.src > Edge.dst).select(Edge, Edge.dst, Edge.src),
... )
>>> m.select(src, dst).to_df()

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Build a semantic model
        ├──  Derive facts with logic
        │   └──  Write conditional definitions with Model.where and Model.define
        └──  Define requirements
            └──  How a requirement works
Model.data(
data: DataFrame | list[tuple] | list[dict], columns: list[str] | None = None
) -> Data

Create a temporary table from in-memory Python or pandas data.

This converts the provided data into a pandas DataFrame and wraps it as a Data object. The resulting object behaves like a table in the semantics DSL: you can refer to its columns in Model.select, Model.where, and Model.define, or convert it to a schema via Table.to_schema.

This method is the underlying implementation used by the convenience wrapper data.

Parameters:

  • data

    (pandas.DataFrame | list[tuple] | list[dict]) - The input data. Supported forms are:

    • A pandas DataFrame
    • A list of tuples (each tuple is a row)
    • A list of dicts (each dict is a row)
  • columns

    (list[str], default: None) - Column names to use when data is a list of tuples. If omitted, the data will use default integer column labels 0, 1, 2, … . Those default labels are exposed as col0, col1, col2, … so you can write d.col0, d.col1, etc.

Returns:

  • Data - A table-like object backed by the provided data.

Raises:

  • TypeError - If data cannot be converted into a pandas DataFrame.

Examples:

Create data from a list of dicts and use it directly:

>>> from relationalai.semantics import Model
>>> m = Model()
>>> people_rows = m.data([
... {"name": "Alice", "age": 10},
... {"name": "Bob", "age": 30},
... ])
>>> m.select(people_rows.name, people_rows.age).to_df()

Provide column names when loading tuple rows:

>>> measurements = m.data([(0, 72.5), (1, 71.9)], columns=["minute", "temperature"])
>>> m.select(measurements.minute, measurements.temperature).to_df()

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Build a semantic model
        ├──  Declare data sources
        │   ├──  Choose the right source type
        │   ├──  Use CSV data with Model.data
        │   ├──  Use a DataFrame with Model.data
        │   └──  Use inline Python data with Model.data
        └──  Define base facts
Model.not_(*items: Statement) -> Not

Return a negated condition for use in query filters.

Use Model.not_ inside Model.where (or Fragment.where) to say “exclude anything that matches this pattern”. For example, m.not_(Person.pets) can be read as “people with no pets”.

If you pass multiple statements, they are treated as a single grouped condition and the whole group is negated. In other words, m.not_(a, b) means “NOT (a AND b)”, not “(NOT a) AND (NOT b)”. Note that NOT (a AND b) is equivalent to (NOT a) OR (NOT b).

Parameters:

Returns:

  • Not - A negation expression.

Examples:

Filter to people who have no pets:

>>> from relationalai.semantics import Model, String
>>> m = Model()
>>> Person = m.Concept("Person", identify_by={"name": String})
>>> Pet = m.Concept("Pet", identify_by={"name": String})
>>> Person.pets = m.Relationship(f"{Person} has {Pet}")
>>> m.define(
... alice := Person.new(name="Alice"),
... bob := Person.new(name="Bob"),
... boots := Pet.new(name="boots"),
... bob.pets(boots),
... )
>>> m.where(Person, m.not_(Person.pets)).select(Person.name).to_df()

Exclude people who have a pet named “boots”:

>>> m.where(Person, m.not_(Person.pets.name == "boots")).select(Person.name).to_df()

Contrast grouped vs separate negation:

>>> # Exclude people people who have no pets and people who have a pet named "boots".
>>> m.where(Person, m.not_(Person.pets, Person.pets.name == "boots"))
>>> # Exclude only people who have no pets.
>>> m.where(Person, m.not_(Person.pets))

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Build a semantic model
        └──  Derive facts with logic
            └──  Write conditional definitions with Model.where and Model.define
Model.distinct(*items: Value) -> Distinct

Mark values as distinct (unique) for use in Model.select and aggregates.

This constructs a Distinct wrapper tied to this model. Use it to request “unique values only” in two common situations:

  • As a SQL SELECT DISTINCT-style wrapper by passing it as the only argument to Model.select, e.g. m.select(m.distinct(x, y)).
  • As an argument wrapper for aggregates to make them count/sum/etc. over unique values, e.g. count(m.distinct(Person.name)).

Parameters:

Returns:

  • Distinct - An object you pass to Model.select or to aggregate functions to request “unique values only”.

Raises:

  • relationalai.util.error.RAIException - Raised when the returned object is used incorrectly (for example, not applied to the entire select(...), or not applied to all aggregate arguments).

Examples:

Count unique names (like SQL COUNT(DISTINCT name)):

>>> from relationalai.semantics import Model, String, count
>>> m = Model()
>>> Person = m.Concept("Person")
>>> Person.name = m.Property(f"{Person} has {String:name}")
>>> m.define(
... Person.new(name="Chris"),
... Person.new(name="Chris"),
... Person.new(name="Joe"),
... )
>>> m.select(count(m.distinct(Person.name))).to_df()

Select unique rows by wrapping all selected values:

>>> name_counts = count(Person).per(Person.name)
>>> m.select(m.distinct(Person.name, name_counts)).to_df()
.
├──  agent > cortex
│   ├──  tool
│   │   ├──  DefaultTool
│   │   └──  ToolRegistry
│   │       └──  add
│   └──  verbalize
│       ├──  ModelVerbalizer
│       └──  SourceCodeVerbalizer
└──  semantics > frontend > base
    ├──  Concept
    ├──  DSLBase
    ├──  Data
    ├──  DerivedTable
    ├──  Distinct
    ├──  Fragment
    ├──  Literal
    ├──  Match
    ├──  Not
    ├──  NumberConcept
    ├──  Property
    ├──  Reading
    ├──  Relationship
    ├──  Table
    ├──  Union
    └──  Variable
RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works
        ├──  Configure PyRel
        │   ├──  Overview
        │   │   ├──  Where configuration comes from
        │   │   └──  How validation works
        │   └──  Configure snowflake auth
        │       └──  Get the connection by creating a session
        └──  Build a semantic model
            ├──  Create a model instance
            │   ├──  Create a Model instance
            │   └──  Configure a model’s connection
            ├──  Declare concepts
            │   ├──  Declare a concept with Model.Concept
            │   ├──  Declare a concept with an identity scheme
            │   ├──  Declare a subconcept with extends
            │   └──  View all concepts declared in a model
            ├──  Declare relationships and properties
            │   ├──  Declare a property with Model.Property
            │   ├──  Declare a relationship with Model.Relationship
            │   └──  View all relationships declared in a model
            ├──  Declare data sources
            │   ├──  Choose the right source type
            │   ├──  Use a Snowflake table with Model.Table
            │   ├──  Use CSV data with Model.data
            │   ├──  Use a DataFrame with Model.data
            │   ├──  Use inline Python data with Model.data
            │   └──  Create model constants with Model.Enum
            ├──  Define base facts
            │   ├──  Define entities and their properties with Concept.new
            │   └──  Define relationship facts
            ├──  Derive facts with logic
            │   ├──  Understand PyRel logic constructs
            │   ├──  Write conditional definitions with Model.where and Model.define
            │   ├──  Compute derived property values
            │   └──  Match multiple entities of the same type with Concept.ref
            ├──  Define requirements
            │   ├──  How a requirement works
            │   ├──  Add a global requirement with Model.require
            │   └──  Add a scoped requirement with Model.where
            └──  Query a model
                ├──  Understand fragments and materialization
                ├──  Select values with select
                ├──  Filter results with where
                └──  Export results to Snowflake tables with into and exec