Skip to content

Compared to ORMs & rules engines

Greffon is not an ORM, so "Greffon vs. X" is mostly a question of which layer each tool owns. This page places it next to the tools people usually arrive from — Prisma, Drizzle, Kysely, TypeORM, MikroORM, and .NET's EF Core — and, because a serializable predicate is also a rule, next to the rules-and-policy tools: JSONLogic, CASL, OPA, Cedar. It is explicit about what Greffon does not do.

The same query, seven ways

A filter, a projection, and an ordering, written in each:

ts
const adults = await db.users
  .filter((u) => u.age >= 18 && u.name.startsWith("A"))
  .map((u) => ({ id: u.id, name: u.name }))
  .orderBy((u) => u.name)
  .toArray();
csharp
var adults = await db.Users
    .Where(u => u.Age >= 18 && u.Name.StartsWith("A"))
    .Select(u => new { u.Id, u.Name })
    .OrderBy(u => u.Name)
    .ToListAsync();
ts
const adults = await prisma.user.findMany({
  where: { age: { gte: 18 }, name: { startsWith: "A" } },
  select: { id: true, name: true },
  orderBy: { name: "asc" },
});
ts
const adults = await db
  .select({ id: users.id, name: users.name })
  .from(users)
  .where(and(gte(users.age, 18), like(users.name, "A%")))
  .orderBy(users.name);
ts
const adults = await db
  .selectFrom("users")
  .select(["id", "name"])
  .where("age", ">=", 18)
  .where("name", "like", "A%")
  .orderBy("name")
  .execute();
ts
const adults = await repo.find({
  select: { id: true, name: true },
  where: { age: MoreThanOrEqual(18), name: Like("A%") },
  order: { name: "ASC" },
});
ts
const adults = await em.find(
  User,
  { age: { $gte: 18 }, name: { $like: "A%" } },
  { fields: ["id", "name"], orderBy: { name: "asc" } },
);

The shapes sort into three families. Prisma, TypeORM, and MikroORM encode the predicate as a data object ({ gte: 18 }, MoreThanOrEqual(18), { $gte: 18 }). Drizzle and Kysely build SQL syntax in TypeScript (gte(users.age, 18), .where("age", ">=", 18)). EF Core and Greffon write an ordinary lambda over the row, which the C# compiler — or Greffon's build plugin — also turns into an expression tree.

The lambda family has one property the other two cannot offer: the predicate is still a function. u => u.age >= 18 && u.name.startsWith("A") runs as-is against plain objects — which is exactly how the memory provider executes it, and why the same query file works under your test runner with no database and no mocks.

What each tool owns

Queries are written asSchema & migrationsChange trackingWritesSame query on plain arrays
GreffonTS lambdas, reified to trees at build timenone — table/column mapping onlynonenone — queries onlyyes — the reference provider
EF Core (.NET)C# lambdas, compiled to treesincludedyes — DbContext + SaveChangesyesyes — LINQ to Objects
Prismadata objects to a generated clientschema.prisma + prisma migrateno — stateless clientyesno
DrizzleSQL-shaped builder over a TS schemaTS schema + drizzle-kitnoyesno
Kyselytyped SQL builder over an interfacemigration runner; types by hand or codegennoyesno
TypeORMfind-options objects, or a string query builderdecorators + migrationsno — save()-based persistenceyesno
MikroORMdata objects (Mongo-style operators)decorators / EntitySchema + migrationsyes — unit of work + identity mapyesno

"Plain arrays" means fixture objects with no engine at all — an in-memory SQLite or PGlite still exercises a database. Greffon's memory provider evaluates the same lambda the query was written with, and it is the reference semantics every SQL provider is property-tested against.

The part only expression trees provide

Every tool above turns its query representation into SQL. Greffon's difference is that the representation is a typed, versioned, JSON-serializable tree with a public schema — an artifact, not an implementation detail:

  • It crosses process boundaries. The same predicate can travel to a server as a remote filter, sit in a store as an auditable policy rule, or be replayed later — serialize / deserialize are part of @greffon/tree's contract, wire format versioned like any other format.
  • Anyone can translate it. A provider is a pure function over a closed grammar (Writing a provider); Postgres and SQLite are the first two targets, not a feature list.
  • EF Core has the tree too — but as an in-process .NET object graph with no wire format; serializing one is a third-party exercise. In Greffon, serialization is the point.

Applications is the catalog of what that buys — authorization rules, filters over the wire, rules-as-data, and running a serialized tree directly against plain objects.

Next to rules engines & policy languages

The ORM shelf is only half the comparison: a typed, serializable predicate is also what rules engines and policy tools provide. The same placement exercise, on that shelf:

  • JSONLogic / json-rules-engine store a rule as JSON operator objects — { "and": [{ ">": [{ "var": "age" }, 18] }] } — untyped, written in a bespoke vocabulary, interpreted at runtime. A Greffon tree is also plain JSON, but it is written as a TypeScript lambda: typed against the row, checked by the compiler, validated against a closed grammar — and the same rule compiles to a SQL WHERE, which a JSONLogic rule cannot do without a hand-written translator.
  • CASL defines isomorphic abilities — "can read Article where authorId = user.id" — with Mongo-style condition objects, and covers both the single-object check and (through its integrations) query filtering. The conditions remain a data DSL: not statically typed against your row types, not a plain function you can call without the library. A tree plays the same role with the predicate written in the language of the codebase, and evaluate, print, and SQL translation come from the one definition.
  • OPA (Rego) / Cedar are separate policy languages with their own evaluators, tooling, and deployment story — the right call when policy must be owned and distributed outside the application. Greffon rules live inside your TypeScript, typed against your schema, and travel as JSON; there is no second language and no engine process — but also no cross-service policy-distribution machinery beyond the JSON itself.

The Applications page calls this policy-as-expression: typed like code, stored and audited like data, translated like a query.

What Greffon deliberately does not do

Stated plainly, because the table compresses it:

  • No writes. There is no insert, update, delete, or save. Pair Greffon with whatever owns writes — your driver, Kysely, Drizzle, an ORM.
  • No schema ownership. Providers take mapping metadata ({ users: { table: "users" } }); nothing generates or migrates tables.
  • No connections. Providers take an executor function; pooling, transactions, and configuration stay with your driver.
  • No change tracker, no identity map, no caches. Rows are plain objects; two queries returning the same row give you two objects.
  • Pre-0.1. The tools above have years of production use behind them; Greffon is new, and says so.

Choosing

  • You want one tool to own schema, migrations, and writes → Prisma, Drizzle, TypeORM, or MikroORM.
  • You want SQL's shape visible in TypeScript → Kysely or Drizzle.
  • You are on .NET → EF Core is this whole design, native to the platform.
  • You want rules stored as data and edited outside a deploy → JSONLogic is the untyped incumbent; a Greffon tree is the typed version of the same idea, with SQL pushdown included.
  • You want policy owned outside the application, distributed across services → OPA or Cedar.
  • You want query lambdas that run against fixtures in tests and compile to parameterized SQL in production, and predicates that serialize into trees a provider can translate → Greffon, next to whatever owns your writes.

These compose rather than compete: nothing stops reads through Greffon and writes through the ORM that owns your schema, over the same tables. Where the ideas themselves come from is its own story — The C# lineage.

MIT licensed. Expression trees for TypeScript.