agnostic-query
Version:
Type-safe fluent builder for portable query schemas. Runtime-agnostic, database-agnostic — the same QuerySchema drives Drizzle, Kysely, db0, or raw SQL.
185 lines (157 loc) • 6.5 kB
text/typescript
import { describe, expect, it } from 'bun:test';
import { PGlite } from '@electric-sql/pglite';
import { drizzle } from 'drizzle-orm/pglite';
import { pgTable, text, integer } from 'drizzle-orm/pg-core';
import { toDrizzleWhere } from './pg.ts';
const users = pgTable('users', {
id: text('id').primaryKey(),
name: text('name'),
age: integer('age'),
tags: text('tags').array(),
data: text('data'),
});
const db = drizzle(new PGlite());
const toSql = (whereExpr: ReturnType<typeof toDrizzleWhere>) => {
return db.select().from(users).where(whereExpr).toSQL();
}
describe('toDrizzleWhere', () => {
it('=', () => {
const sql = toSql(toDrizzleWhere(users, { field: ['name'], op: '=', value: 'Alice' }));
expect(sql.sql).toBe(`select "id", "name", "age", "tags", "data" from "users" where "users"."name" = $1`);
expect(sql.params).toEqual(['Alice']);
});
it('>', () => {
const sql = toSql(toDrizzleWhere(users, { field: ['age'], op: '>', value: 18 }));
expect(sql.sql).toBe(`select "id", "name", "age", "tags", "data" from "users" where "users"."age" > $1`);
expect(sql.params).toEqual([18]);
});
it('>=', () => {
const sql = toSql(toDrizzleWhere(users, { field: ['age'], op: '>=', value: 18 }));
expect(sql.sql).toBe(`select "id", "name", "age", "tags", "data" from "users" where "users"."age" >= $1`);
});
it('<', () => {
const sql = toSql(toDrizzleWhere(users, { field: ['age'], op: '<', value: 18 }));
expect(sql.sql).toBe(`select "id", "name", "age", "tags", "data" from "users" where "users"."age" < $1`);
});
it('<=', () => {
const sql = toSql(toDrizzleWhere(users, { field: ['age'], op: '<=', value: 18 }));
expect(sql.sql).toBe(`select "id", "name", "age", "tags", "data" from "users" where "users"."age" <= $1`);
});
it('like', () => {
const sql = toSql(toDrizzleWhere(users, { field: ['name'], op: 'like', value: '%test%' }));
expect(sql.sql).toBe(`select "id", "name", "age", "tags", "data" from "users" where "users"."name" like $1`);
});
it('ilike', () => {
const sql = toSql(toDrizzleWhere(users, { field: ['name'], op: 'ilike', value: '%Test%' }));
expect(sql.sql).toBe(`select "id", "name", "age", "tags", "data" from "users" where "users"."name" ilike $1`);
});
it('is null', () => {
const sql = toSql(toDrizzleWhere(users, { field: ['name'], op: 'is null' }));
expect(sql.sql).toBe(`select "id", "name", "age", "tags", "data" from "users" where "users"."name" is null`);
});
it('@> (contains)', () => {
const sql = toSql(toDrizzleWhere(users, { field: ['tags'], op: '@>', value: ['admin'] }));
expect(sql.sql).toBe(`select "id", "name", "age", "tags", "data" from "users" where "users"."tags" @> $1`);
});
it('<@ (contained by)', () => {
const sql = toSql(toDrizzleWhere(users, { field: ['tags'], op: '<@', value: ['admin', 'user'] }));
expect(sql.sql).toBe(`select "id", "name", "age", "tags", "data" from "users" where "users"."tags" <@ $1`);
});
it('&& (overlaps)', () => {
const sql = toSql(toDrizzleWhere(users, { field: ['tags'], op: '&&', value: ['admin'] }));
expect(sql.sql).toBe(`select "id", "name", "age", "tags", "data" from "users" where "users"."tags" && $1`);
});
it('in', () => {
const sql = toSql(toDrizzleWhere(users, { field: ['id'], op: 'in', values: ['1', '2', '3'] }));
expect(sql.sql).toBe(`select "id", "name", "age", "tags", "data" from "users" where "users"."id" in ($1, $2, $3)`);
expect(sql.params).toEqual(['1', '2', '3']);
});
it('and', () => {
const sql = toSql(toDrizzleWhere(users, {
op: 'and',
conditions: [
{ field: ['name'], op: '=', value: 'Alice' },
{ field: ['age'], op: '>', value: 18 },
],
}));
expect(sql.sql).toBe(`select "id", "name", "age", "tags", "data" from "users" where ("users"."name" = $1 and "users"."age" > $2)`);
expect(sql.params).toEqual(['Alice', 18]);
});
it('or', () => {
const sql = toSql(toDrizzleWhere(users, {
op: 'or',
conditions: [
{ field: ['name'], op: '=', value: 'Alice' },
{ field: ['name'], op: '=', value: 'Bob' },
],
}));
expect(sql.sql).toBe(`select "id", "name", "age", "tags", "data" from "users" where ("users"."name" = $1 or "users"."name" = $2)`);
});
it('not', () => {
const sql = toSql(toDrizzleWhere(users, {
op: 'not',
condition: { field: ['age'], op: '<', value: 18 },
}));
expect(sql.sql).toBe(`select "id", "name", "age", "tags", "data" from "users" where not "users"."age" < $1`);
});
it('nested and/or/not', () => {
const sql = toSql(toDrizzleWhere(users, {
op: 'and',
conditions: [
{
op: 'or',
conditions: [
{ field: ['name'], op: 'like', value: '%test%' },
{ op: 'not', condition: { field: ['age'], op: '=', value: 0 } },
],
},
{ field: ['id'], op: 'in', values: ['a', 'b'] },
],
}));
expect(sql.sql).toBe(`select "id", "name", "age", "tags", "data" from "users" where (("users"."name" like $1 or not "users"."age" = $2) and "users"."id" in ($3, $4))`);
});
it('undefined input returns undefined', () => {
expect(toDrizzleWhere(users)).toBeUndefined();
});
it('returns undefined for non-existent column', () => {
expect(toDrizzleWhere(users, { field: ['unknown'], op: '=', value: 'x' } as any)).toBeUndefined();
});
it('handles multi-segment JSON path', () => {
const sql = toSql(
toDrizzleWhere(users, {
field: ['data', 'address', 'city'],
op: '=',
value: 'NYC',
}),
);
expect(sql.sql).toContain(`"data"->'address'->>'city'`);
});
});
import { toDrizzleOrderBy } from './pg.ts';
describe('toDrizzleOrderBy', () => {
it('single asc', () => {
const result = toDrizzleOrderBy(users, [
{ field: ['name'], direction: 'asc' },
]);
const sql = db.select().from(users).orderBy(...result!).toSQL();
expect(sql.sql).toContain('order by "users"."name"');
});
it('single desc', () => {
const result = toDrizzleOrderBy(users, [
{ field: ['age'], direction: 'desc' },
]);
const sql = db.select().from(users).orderBy(...result!).toSQL();
expect(sql.sql).toContain('order by "users"."age" desc');
});
it('multiple clauses', () => {
const result = toDrizzleOrderBy(users, [
{ field: ['name'], direction: 'asc' },
{ field: ['age'], direction: 'desc' },
]);
const sql = db.select().from(users).orderBy(...result!).toSQL();
expect(sql.sql).toContain('order by "users"."name" asc, "users"."age" desc');
});
it('null returns empty array', () => {
expect(toDrizzleOrderBy(users)).toEqual([]);
});
});