Created
November 6, 2025 22:21
-
-
Save postspectacular/ff997a4f1016b17bbfe9beb989984ac3 to your computer and use it in GitHub Desktop.
Query engine example using a Lisp-like syntax to perform nested tag queries with Set-semantics (TODO insert URL)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import type { FnU2, Maybe, Predicate2 } from "@thi.ng/api"; | |
| import { difference, intersection, union } from "@thi.ng/associative"; | |
| import { isArray, isString } from "@thi.ng/checks"; | |
| import { illegalArgs } from "@thi.ng/errors"; | |
| import { evalSource } from "@thi.ng/lispy"; | |
| import { defQuery, type QueryOpts } from "@thi.ng/oquery"; | |
| import { expect, test } from "bun:test"; | |
| type Note = { id: number; tags: string[] }; | |
| // example collection of tagged notes/items for querying | |
| const DB = <Note[]>[ | |
| { id: 0, tags: ["a", "b"] }, | |
| { id: 1, tags: ["a", "foo"] }, | |
| { id: 2, tags: ["b", "bar"] }, | |
| { id: 3, tags: ["c"] }, | |
| ]; | |
| // helper function for testing | |
| const expectQuery = (src: string, ...expectedIDs: number[]) => | |
| expect(executeQuery(DB, src)).toEqual(expectedIDs.map((i) => DB[i])); | |
| // test various query expressions... | |
| test("and", () => expectQuery("(and 'a' 'b')", 0)); | |
| test("or", () => expectQuery("(or 'a' 'b')", 0, 1, 2)); | |
| test("not", () => expectQuery("(not 'a')", 2, 3)); | |
| test("nested and", () => expectQuery("(and (not 'a') 'c')", 3)); | |
| test("nested or", () => expectQuery("(not (or 'foo' 'bar'))", 0, 3)); | |
| test("double negative", () => expectQuery("(not (not 'a'))", 0, 1)); | |
| // main query function, evaluating (potentially nested) query expressions in | |
| // Lisp-style syntax, e.g. `(not (or 'tag1' 'tag2'))` to select items which | |
| // have neither a `tag1` nor `tag2`... | |
| const executeQuery = (db: Note[], src: string) => | |
| // evalSource() is the thi.ng/lispy interpreter which takes the query | |
| // expression sourcecode and a custom environment of available built-in | |
| // functions, in this case limited to only the following three ops... | |
| evalSource( | |
| src, | |
| { | |
| // implementation of AND (aka intersection) query (see below) | |
| and: defOp( | |
| db, | |
| intersection, | |
| (terms, tags) => terms.every((x) => tags.includes(x)), | |
| { intersect: true, cwise: false } | |
| ), | |
| // implementation of OR (aka union) query | |
| or: defOp( | |
| db, | |
| union, | |
| (terms, tags) => terms.some((x) => tags.includes(x)), | |
| { cwise: false } | |
| ), | |
| // implementation of NOT (aka negation/difference) query | |
| not: (term: any) => | |
| isString(term) | |
| ? defQuery<Note[]>({ intersect: true, cwise: false })( | |
| db, | |
| "tags", | |
| (x: string[]) => !x.includes(term) | |
| ) | |
| : isArray(term) | |
| ? [...difference(new Set(db), new Set(term))] | |
| : illegalArgs("wrong NOT query term"), | |
| }, | |
| // custom syntax option for our query DSL, see https://thi.ng/lispy | |
| { string: "'" } | |
| ); | |
| // higher-order query operation (used for AND/OR queries). | |
| // delegates to https://thi.ng/oquery for querying | |
| // applies chosen Set semantics to combine any sub-results | |
| const defOp = | |
| ( | |
| db: Note[], | |
| op: FnU2<Set<Note>>, | |
| pred: Predicate2<string[]>, | |
| opts: Partial<QueryOpts> | |
| ) => | |
| (...terms: any[]) => { | |
| let items: Maybe<Set<Note>>; | |
| const tags: string[] = []; | |
| for (let x of terms) { | |
| if (isString(x)) tags.push(String(x)); | |
| else if (isArray(x)) | |
| items = items ? op(items, new Set(x)) : new Set(x); | |
| else illegalArgs("wrong query term: " + x); | |
| } | |
| const sel = items ? [...items] : db; | |
| return tags.length | |
| ? defQuery<Note[]>(opts)(sel, "tags", (x: string[]) => | |
| pred(tags, x) | |
| ) | |
| : sel; | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment