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.