Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,8 @@ jobs:
fi
done

- name: Generate llms-full.txt
run: bash scripts/generate-llms-full.sh
working-directory: website

# llms-full.txt is regenerated by the prebuild script that npm runs ahead of
# the build, so it needs no step of its own here.
- name: Build Docusaurus
run: npm run build
working-directory: website
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ website/node_modules/
website/build/
website/.docusaurus/
website/static/api/
# Generated from docs/ by website/scripts/generate-llms-full.sh, which runs as the
# prebuild step of every site build. Committing it only lets it drift.
website/static/llms-full.txt

# Working analysis doc — not for check-in
ADOPTION_ANALYSIS.md
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ Everything you need to build applications with Storm. Start with Getting Started
| [Queries](docs/queries.md) | Select, filter, aggregate, order (8 min) |
| [Metamodel](docs/metamodel.md) | Compile-time type safety (10 min) |
| [Refs](docs/refs.md) | Lazy loading and optimized references (7 min) |
| [Entity Design](docs/entity-design.md) | Choosing between inlined foreign keys and Refs (9 min) |
| [Batch & Streaming](docs/batch-streaming.md) | Bulk operations and Flow/Stream (5 min) |
| [Upserts](docs/upserts.md) | Insert-or-update operations (6 min) |
| [Write Sets](docs/write-sets.md) | Dependency-ordered writes of mixed-type entity graphs (7 min) |
Expand Down
2 changes: 2 additions & 0 deletions docs/entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,8 @@ Nullability affects how relationships are loaded:
- **Non-nullable FK:** INNER JOIN (referenced entity must exist)
- **Nullable FK:** LEFT JOIN (referenced entity may be null)

Foreign keys are joined transitively, so an entity's reads carry the columns of everything its graph reaches. Declaring a foreign key as [`Ref`](refs.md) stores the key and leaves the join to the queries that ask for it. See [Entity Design](entity-design.md) for when that is worth doing.

---

## Suppressing Schema Validation
Expand Down
251 changes: 251 additions & 0 deletions docs/entity-design.md

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,45 @@ Every executed statement is logged, whichever repository, query builder or templ

See [SQL Logging](sql-logging.md) for the full guide.

### The Kotlin compiler crashes with "Unexpected FirPlaceholderProjectionImpl"

This is a Kotlin 2.0.x compiler bug, fixed in **Kotlin 2.1.0**. It reports an unresolved call as an internal error
rather than a diagnostic, whenever the call uses `_` type-argument placeholders:

```
org.jetbrains.kotlin.util.FileAnalysisException: While analysing Foo.kt:12:9:
Unexpected FirPlaceholderProjectionImpl
```

On 2.1.0 and later the same code reports `Unresolved reference 'select'`, which names the real problem. Upgrading
the Kotlin version is the fix; nothing about Storm changes.

If you are on 2.0.x, read the crash as "something in this call does not resolve". It names nothing you wrote, so it
can stand in for any resolution failure at that call site. Two causes account for most of them.

**A dependency compiled by a newer Kotlin.** A 2.0.x compiler cannot read metadata from Kotlin 2.2 or later, so every
call into that library becomes unresolved. Look for this line above the crash, which is the one that actually explains
it:

```
Module was compiled with an incompatible version of Kotlin.
The binary version of its metadata is 2.2.0, expected version is 2.0.0.
```

A 2.1-built dependency is fine; 2.2 is where it breaks.

**A missing import**, because repository operations are top-level extension functions:

```kotlin
import st.orm.repository.*
```

`import st.orm.template.*` does not bring them in, since they live in `st.orm.repository`. Import both packages with
wildcards rather than naming functions individually. Without it, a call such as `select<UserSummary, _, _> { ... }`
has no applicable candidate at all: the extension is out of scope, and no `select` member matches a call that passes
a template lambda and three type arguments. Replacing the placeholders with explicit type arguments turns the crash
back into a readable error on 2.0.x, which is a quick way to find the cause without upgrading.

### Schema validation reports type narrowing warnings for my Integer columns

Some databases (notably Oracle) use a single numeric type for all integer columns. For example, Oracle's `NUMBER` maps to `java.sql.Types.NUMERIC`, which Storm considers a "narrowing" conversion for `Integer` fields. These are logged as warnings because the mapping works at runtime but may involve precision differences.
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ If you are new to Storm, follow these guides in order to build a solid foundatio
5. [Queries](queries.md) -- the full query DSL and builder reference
6. [Repositories](repositories.md) -- the repository pattern and custom query methods
7. [Relationships](relationships.md) -- foreign keys, entity graphs, and many-to-many
8. [Entity Design](entity-design.md) -- when to inline a foreign key and when to reach for a Ref

### Migrating from JPA

Expand Down
22 changes: 17 additions & 5 deletions docs/refs.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Refs are lightweight identifiers for entities, projections, and other data types

Unlike a typical lazy reference, a `Ref` never trades away query capability. Filter, order, group, and select through it with the metamodel exactly as you would through a directly-referenced entity, and Storm adds the join only for the query that actually needs it. When a query already knows it needs the referenced record, it can resolve the `Ref` as part of that same statement instead of paying for a separate fetch. Choosing `Ref<T>` over the entity type is a decision about the SELECT you get by default, not a capability you give up.

That said, it is a decision worth making sparingly. Joins on primary keys are cheap, so the default is to model foreign keys as direct types and reach for a `Ref` when an edge lets the graph grow beyond what your reads should carry. [Entity Design](entity-design.md) covers where that line sits and how Storm's schema-first generation draws it.

---

## Using Refs in Entities
Expand Down Expand Up @@ -545,7 +547,7 @@ Use `Ref.of(entity)` when you already have the entity in memory and want to wrap

## Aggregation with Refs

Refs are particularly useful in aggregation queries where you group by a foreign key. Instead of loading the full related entity for each group, you can select only the primary key as a Ref. This keeps the query lightweight while still giving you a typed identifier to use in subsequent lookups if needed.
Refs are particularly useful in aggregation queries where you group by a foreign key. Instead of loading the full related entity for each group, name the foreign key path and select it as a Ref. This keeps the query lightweight while still giving you a typed identifier to use in subsequent lookups if needed.

<Tabs groupId="language">
<TabItem value="kotlin" label="Kotlin" default>
Expand All @@ -557,7 +559,7 @@ data class GroupedByCity(
)

val counts: Map<Ref<City>, Long> = orm.entity<User>()
.select<GroupedByCity, _, _> { "${select(City::class, SelectMode.PK)}, COUNT(*)" }
.select<GroupedByCity, _, _> { "${User_.city}, COUNT(*)" }
.groupBy(User_.city)
.resultList
.associate { it.city to it.count }
Expand All @@ -570,7 +572,7 @@ val counts: Map<Ref<City>, Long> = orm.entity<User>()
record GroupedByCity(Ref<City> city, long count) {}

Map<Ref<City>, Long> counts = orm.entity(User.class)
.select(GroupedByCity.class, RAW."\{select(City.class, SelectMode.PK)}, COUNT(*)")
.select(GroupedByCity.class, RAW."\{User_.city}, COUNT(*)")
.groupBy(User_.city)
.getResultList().stream()
.collect(toMap(GroupedByCity::city, GroupedByCity::count));
Expand All @@ -580,7 +582,7 @@ Using SQL Templates:

```java
Map<Ref<City>, Long> counts = orm.query(RAW."""
SELECT \{select(City.class, SelectMode.PK)}, COUNT(*)
SELECT \{User_.city}, COUNT(*)
FROM \{User.class}
GROUP BY \{User_.city}""")
.getResultList(GroupedByCity.class).stream()
Expand All @@ -592,6 +594,16 @@ Map<Ref<City>, Long> counts = orm.query(RAW."""

The database does the aggregating, so one row per city comes back and the reference carries the key you group by. Fetch the cities themselves with `findAllByRef(counts.keys)` when a later step needs them.

Naming the foreign key path selects the key column on the user table itself, so the referenced table is never joined:

```sql
SELECT u.city_id, COUNT(*) FROM user u GROUP BY u.city_id
```

The key does not have to be a Ref. Selecting the entity itself, `${City::class}` in place of `${User_.city}` with a `City` component instead of `Ref<City>`, produces a `Map<City, Long>` whose keys carry their data, with no later fetch. That form pays for the join, for the city columns on every row, and for a `GROUP BY` over all of them, and its keys hash across every field rather than the primary key alone. Prefer the Ref form unless you need the records and would have fetched them anyway.

This is not the same as [`resultGroupedBy`](queries.md#grouped-results). That variant does not change the SQL: it runs the same select and groups the hydrated rows, returning each city with the users that belong to it. Reach for it when you want the records themselves, and aggregate in the database, as above, when you only want a number per group.

---

## Use Cases
Expand Down Expand Up @@ -652,7 +664,7 @@ Understanding how `fetch()` resolves its target helps you predict performance an

## Tips

1. **Use Refs for optional relationships.** Avoid loading data you might not need.
1. **Reach for a Ref when an edge lets the graph grow.** A join on its own is not a reason, and neither is an optional relationship. Optional edges are simply the natural first cut once a graph does need trimming. See [Entity Design](entity-design.md).
2. **Use Refs for self-references.** Prevent circular loading in hierarchical data.
3. **Use Refs in aggregations.** Get counts by FK without loading full entities.
4. **Refs are reliable map keys.** They provide lightweight, identity-based comparison.
Expand Down
6 changes: 4 additions & 2 deletions docs/relationships.md
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,7 @@ Storm's approach:

### Managing Graph Depth

For deep or circular relationships, use `Ref` to break the loading chain:
Circular relationships force a `Ref`, and a graph that has grown genuinely wide can use one to break the loading chain:

```kotlin
data class Category(
Expand All @@ -656,9 +656,11 @@ A `Ref` removes the join from every read but keeps the relationship queryable: y

The self-reference above is navigable too: `Category_.parent.name` joins the category table to itself, so it filters on the parent's name. The typed metamodel navigates a cycle two hops deep; deeper cyclic paths are named as strings. See [Refs](refs.md), [Querying Through Refs](refs.md#querying-through-refs) and [Cyclic References](metamodel.md#cyclic-references) for details.

Deciding which edges to cut, and which to leave inlined, is a design question in its own right. See [Entity Design](entity-design.md).

## Tips

1. **Keep entity graphs shallow.** Deep graphs mean large JOINs. Use `Ref` for optional or deep relationships.
1. **Model foreign keys as direct types by default.** A join on a primary key is cheap, and one query beats several. Reach for `Ref` when an edge lets the graph grow, not because a join exists. See [Entity Design](entity-design.md).
2. **Query the "many" side.** For one-to-many, query the child entity with a filter on the parent.
3. **Use join entities for many-to-many.** Explicit join tables give you control over the relationship.
4. **Match nullability to your schema.** Use nullable FKs only when the database column allows NULL.
Expand Down
1 change: 1 addition & 0 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"prebuild": "bash scripts/generate-llms-full.sh",
"build": "docusaurus build",
"serve": "docusaurus serve",
"clear": "docusaurus clear"
Expand Down
1 change: 1 addition & 0 deletions website/scripts/generate-llms-full.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ DOCS=(
pagination-and-scrolling.md
metamodel.md
refs.md
entity-design.md
transactions.md
spring-integration.md
dialects.md
Expand Down
1 change: 1 addition & 0 deletions website/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const sidebars: SidebarsConfig = {
'pagination-and-scrolling',
'metamodel',
'refs',
'entity-design',
'transactions',
'spring-integration',
'ktor-integration',
Expand Down
Loading
Loading