Hierarchy checks
Filter by branch or subtree with ancestor and descendant checks.
| path.isAncestorOf(rhs) | ltree @> ltree |
| path.isDescendantOf(rhs) | ltree <@ ltree |
Prisma Next extension pack
PostgreSQL's ltree hierarchical-tree type for Prisma Next. Model category trees, org charts, and taxonomies, then query them with typed operators. No raw SQL.
pnpm add prisma-ltreeSee it work
Each operator selects a different slice of the tree. Pick one below to see which nodes match, the typed call, and the SQL it runs.
Filter a whole branch with one path.
// All categories in the Top.Science branch
sql.from(tables.category)
.where(tables.category.columns.path.isDescendantOf("Top.Science"));path.isDescendantOf("Top.Science")path <@ 'Top.Science'Descendants: path.isDescendantOf("Top.Science"). 4 of 8 nodes match.
Quickstart
Requires Node >=24 and @prisma-next/*@0.14.0. Full setup in the Getting Started guide.
Add prisma-ltree/control to your config.
// prisma-next.config.ts
import { defineConfig } from "@prisma-next/cli/config-types";
import postgresAdapter from "@prisma-next/adapter-postgres/control";
import sql from "@prisma-next/family-sql/control";
import postgres from "@prisma-next/target-postgres/control";
import ltree from "prisma-ltree/control";
export default defineConfig({
family: sql,
target: postgres,
adapter: postgresAdapter,
extensionPacks: [ltree],
});Use ltree() in TypeScript or ltree.Ltree() in PSL. See both lanes.
// TypeScript lane — PSL uses ltree.Ltree() in contract.prisma
import { int4Column, textColumn } from "@prisma-next/adapter-postgres/column-types";
import { defineContract, field, model } from "@prisma-next/sql-contract-ts/contract-builder";
import { ltree } from "prisma-ltree/column-types";
import ltreePack from "prisma-ltree/pack";
export const contract = defineContract({
family: sqlFamily,
target: postgres,
extensionPacks: { ltree: ltreePack },
models: {
Category: model("Category", {
fields: {
id: field.column(int4Column).id(),
name: field.column(textColumn),
path: field.column(ltree()),
},
}).sql({ table: "category" }),
},
});Operators attach to ltree column references in the query builder.
// Find every category under "electronics"
import { param } from "@prisma-next/sql-query/param";
import { db } from "./prisma/db";
const category = db.schema.tables.category;
const plan = db.sql
.from(category)
.select({ id: category.columns.id, path: category.columns.path })
.where(category.columns.path.isDescendantOf(param("prefix")))
.build({ params: { prefix: "electronics" } });
const rows = await db.runtime().execute(plan);Operations
Each method maps to a native PostgreSQL operator or function. Full reference in the docs.
Filter by branch or subtree with ancestor and descendant checks.
| path.isAncestorOf(rhs) | ltree @> ltree |
| path.isDescendantOf(rhs) | ltree <@ ltree |
Match paths with lquery wildcards, multiple lquery patterns, or full-text ltxtquery expressions.
| path.matchesLquery(pattern) | ltree ~ lquery |
| path.matchesLqueryArray(patterns) | ltree ? lquery[] |
| path.matchesLtxtquery(query) | ltree @ ltxtquery |
Derive depth, slice subpaths, find a label's index, or compute the lowest common ancestor.
| path.nlevel() | nlevel(ltree) |
| path.subltree(start, end) | subltree(ltree, start, end) |
| path.subpath(offset, len?) | subpath(ltree, offset, len) |
| path.indexOf(other, off?) | index(ltree, ltree, off) |
| path.lca(other, ...rest) | lca(ltree, ltree, ...) |
Build new paths by concatenating ltrees or text labels, and convert between ltree and text in either direction.
| path.concat(rhs) | ltree || ltree |
| path.concatText(label) | ltree || text |
| path.prependText(label) | text || ltree |
| path.toText() | ltree2text(ltree) |
| text.toLtree() | text2ltree(text) |
Store multiple paths in an ltree[] column, then pick the first match for containment or patterns.
| paths.firstAncestorOf(rhs) | ltree[] ?@> ltree |
| paths.firstDescendantOf(rhs) | ltree[] ?<@ ltree |
| paths.firstMatchLquery(pattern) | ltree[] ?~ lquery |
| paths.firstMatchLtxtquery(query) | ltree[] ?@ ltxtquery |