thi-ng/umbrella

View on GitHub
packages/oquery/src/query.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import type { Fn2, Nullable, Predicate } from "@thi.ng/api";
import { isArray } from "@thi.ng/checks/is-array";
import { isFunction } from "@thi.ng/checks/is-function";
import { isSet } from "@thi.ng/checks/is-set";
import { compare } from "@thi.ng/compare/compare";
import { compareByKey } from "@thi.ng/compare/keys";
import { reverse } from "@thi.ng/compare/reverse";
import { defmulti } from "@thi.ng/defmulti/defmulti";
import { equiv } from "@thi.ng/equiv";
import type {
    ArrayQueryFn,
    FTerm,
    KeyQueryFn,
    KeyQueryOpts,
    MultiQueryOpts,
    OTerm,
    QueryFn,
    QueryImpl,
    QueryImpls,
    QueryObj,
    QueryOpts,
    QueryTerm,
    QueryType,
    SPInputTerm,
    SPTerm,
} from "./api.js";

/**
 * Classifies a single S,P,O term. See {@link QueryType} for explanations.
 *
 * @param x -
 *
 * @internal
 */
const __classify = (x: any) => (x != null ? (isFunction(x) ? "f" : "l") : "n");

/** @internal */
const __ensureArray = (src: any[] | Set<any>) =>
    isArray(src) ? src : [...src];

/** @internal */
const __ensureSet = (src: any[] | Set<any>) =>
    isArray(src) ? new Set(src) : src;

/**
 * HOF predicate function for intersection queries (i.e. iff `opts.intersect =
 * true`). Takes an array or set query term and returns predicate to check if
 * ALL of the query term elements are present in a given object (assumed to be
 * an array or set too).
 *
 * @param src -
 *
 * @internal
 */
const __intersect = (src: any[] | Set<any>) => {
    const a = __ensureArray(src);
    const num = a.length;
    return (b: any) => {
        const $b = __ensureSet(b);
        for (let i = num; i-- > 0; ) {
            if (!$b.has(a[i])) return false;
        }
        return true;
    };
};

/**
 * Checks the type of a single S,P,O term and if an array or Set, returns a
 * predicate function to check if an element is included. Otherwise, returns
 * input. If `isec` is true, the returned predicate will be the result of
 * {@link __intersect}.
 *
 * @param x -
 * @param isec -
 *
 * @internal
 */
const __coerce = (x: any, isec = false) =>
    isArray(x)
        ? isec
            ? __intersect(x)
            : (y: any) => x.includes(y)
        : isSet(x)
        ? isec
            ? __intersect(x)
            : (y: any) => x.has(y)
        : x;

/**
 * Similar to {@link __coerce}, but intended for S,P terms. Unless `x` is a
 * function or null, coerces `x` (or its elements) to strings first.
 *
 * @param x -
 *
 * @internal
 */
const __coerceStr = (x: any) =>
    isArray(x)
        ? __coerce(x.map((y) => String(y)))
        : isSet(x)
        ? __coerce(new Set([...x].map((y) => String(y))))
        : x == null || isFunction(x)
        ? x
        : String(x);

/** @internal */
const __addTriple = (acc: any, s: any, p: any, o: any) => {
    const sval = acc[s];
    sval ? (sval[p] = o) : (acc[s] = { [p]: o });
};

/** @internal */
const __match = (o: any, val: any, opts: QueryOpts) => {
    if (val != null) {
        const pred = <Predicate<any>>(
            (isFunction(o) ? o : ($: any) => opts.equiv(o, $))
        );
        return opts.cwise && isArray(val) ? val.some(pred) : pred(val);
    }
    return false;
};

/** @internal */
const __collect = (
    acc: any,
    s: any,
    p: any,
    o: any,
    val: any,
    opts: QueryOpts
) => {
    if (val != null) {
        const pred = isFunction(o) ? o : ($: any) => opts.equiv(o, $);
        if (opts.cwise && isArray(val)) {
            val = val.filter(pred);
            val.length && __addTriple(acc, s, p, val);
        } else if (pred(val)) {
            __addTriple(acc, s, p, val);
        }
    }
};

/** @internal */
const __collectFull = (res: QueryObj, s: any, val: any) => (res[s] = val);

/** @internal */
const __collectSP = (
    res: QueryObj,
    sval: any,
    s: SPTerm,
    p: SPTerm,
    o: any,
    opts: QueryOpts
) => {
    if (opts.partial) {
        for (let $p in sval) {
            (<FTerm>p)($p) && __collect(res, s, $p, o, sval[$p], opts);
        }
    } else {
        for (let $p in sval) {
            if ((<FTerm>p)($p) && __match(o, sval[$p], opts)) {
                __collectFull(res, s, sval);
                return;
            }
        }
    }
};

/** @internal */
const __collectSO = (
    res: QueryObj,
    sval: any,
    s: SPTerm,
    o: any,
    opts: QueryOpts
) => {
    if (opts.partial) {
        for (let p in sval) {
            __collect(res, s, p, o, sval[p], opts);
        }
    } else {
        for (let p in sval) {
            if (__match(o, sval[p], opts)) {
                __collectFull(res, s, sval);
                return;
            }
        }
    }
};

/** @internal */
const __queryLL: QueryImpl = (res, db: any, s, p, o, opts) => {
    const sval = db[<any>s];
    const val = sval?.[<string>p];
    if (opts.partial) {
        __collect(res, s, p, o, val, opts);
    } else {
        __match(o, val, opts) && __collectFull(res, s, sval);
    }
};

/** @internal */
const __queryLF: QueryImpl = (res, db: any, s, p, o, opts) => {
    const sval = db[<string>s];
    sval != null && __collectSP(res, sval, s, p, o, opts);
};

/** @internal */
const __queryLN: QueryImpl = (res, db: any, s, _, o, opts) => {
    const sval = db[<string>s];
    sval != null && __collectSO(res, sval, s, o, opts);
};

/** @internal */
const __queryFL: QueryImpl = (res, db: any, s, p, o, opts) => {
    if (opts.partial) {
        for (let $s in db) {
            (<FTerm>s)($s) &&
                __collect(res, $s, p, o, db[$s]?.[<string>p], opts);
        }
    } else {
        for (let $s in db) {
            const sval = db[$s];
            (<FTerm>s)($s) &&
                __match(o, sval?.[<string>p], opts) &&
                __collectFull(res, $s, sval);
        }
    }
};

/** @internal */
const __queryFF: QueryImpl = (res, db: any, s, p, o, opts) => {
    for (let $s in db) {
        (<FTerm>s)($s) && __collectSP(res, db[$s], $s, p, o, opts);
    }
};

/** @internal */
const __queryFFNPartial: QueryImpl = (res, db: any, s, p) => {
    for (let $s in db) {
        if ((<FTerm>s)($s)) {
            const sval = db[$s];
            for (let $p in sval) {
                (<FTerm>p)($p) && __addTriple(res, $s, $p, sval[$p]);
            }
        }
    }
};

/** @internal */
const __queryFFN: QueryImpl = (res, db: any, s, p) => {
    for (let $s in db) {
        if ((<FTerm>s)($s)) {
            const sval = db[$s];
            for (let $p in sval) {
                if ((<FTerm>p)($p)) {
                    __collectFull(res, $s, sval);
                    break;
                }
            }
        }
    }
};

/** @internal */
const __queryFN: QueryImpl = (res, db: any, s, _, o, opts) => {
    for (let $s in db) {
        (<FTerm>s)($s) && __collectSO(res, db[$s], $s, o, opts);
    }
};

/** @internal */
const __queryNL: QueryImpl = (res, db: any, _, p, o, opts) => {
    if (opts.partial) {
        for (let s in db) {
            __collect(res, s, p, o, db[s][<string>p], opts);
        }
    } else {
        for (let s in db) {
            const sval = db[s];
            __match(o, sval[<string>p], opts) && __collectFull(res, s, sval);
        }
    }
};

/** @internal */
const __queryNF: QueryImpl = (res, db: any, _, p, o, opts) => {
    for (let s in db) {
        __collectSP(res, db[s], s, p, o, opts);
    }
};

/** @internal */
const __queryNN: QueryImpl = (res, db: any, _, __, o, opts) => {
    for (let s in db) {
        __collectSO(res, db[s], s, o, opts);
    }
};

/** @internal */
const __queryNLNPartial: QueryImpl = (res, db: any, _, p) => {
    for (let s in db) {
        const val = db[s][<string>p];
        val != null && __addTriple(res, s, p, val);
    }
};

/** @internal */
const __queryNLN: QueryImpl = (res, db: any, _, p) => {
    for (let s in db) {
        const sval = db[s];
        const val = sval[<string>p];
        val != null && __collectFull(res, s, sval);
    }
};

/** @internal */
const __querySP: QueryImpl = (res, sval: any, s, p, _, opts) => {
    if (opts.partial) {
        for (let q in sval) {
            if ((<FTerm>p)(q)) {
                const val = sval[q];
                val != null && __addTriple(res, s, q, val);
            }
        }
    } else {
        for (let q in sval) {
            if ((<FTerm>p)(q)) {
                __collectFull(res, s, sval);
                return;
            }
        }
    }
};

/** @internal */
const __queryO: QueryImpl = (res, db: any, s, p, _, opts) => {
    const sval = db[<string>s];
    const val = sval?.[<string>p];
    val != null &&
        (opts.partial
            ? __addTriple(res, s, p, val)
            : __collectFull(res, s, sval));
};

/** @internal */
const IMPLS = <QueryImpls>{
    lll: __queryLL,
    llf: __queryLL,
    lln: __queryO,
    lfl: __queryLF,
    lff: __queryLF,
    lfn: (res, db: any, s, p, _, opts) => {
        const sval = db[<string>s];
        sval != null && __querySP(res, sval, s, p, null, opts);
    },
    lnl: __queryLN,
    lnf: __queryLN,
    lnn: (res, db: any, s) => {
        const sval = db[<string>s];
        sval != null && __collectFull(res, s, sval);
    },
    fll: __queryFL,
    flf: __queryFL,
    fln: (res, db, s, p, _, opts) => {
        for (let $s in db) {
            (<FTerm>s)($s) && __queryO(res, db, $s, p, null, opts);
        }
    },
    ffl: __queryFF,
    fff: __queryFF,
    ffn: (res, db: any, s, p, o, opts) =>
        (opts.partial ? __queryFFNPartial : __queryFFN)(res, db, s, p, o, opts),
    fnl: __queryFN,
    fnf: __queryFN,
    fnn: (res, db: any, s) => {
        for (let $s in db) {
            if ((<FTerm>s)($s)) {
                const sval = db[$s];
                sval != null && __collectFull(res, $s, sval);
            }
        }
    },
    nll: __queryNL,
    nlf: __queryNL,
    nln: (res, db: any, s, p, o, opts) =>
        (opts.partial ? __queryNLNPartial : __queryNLN)(res, db, s, p, o, opts),
    nfl: __queryNF,
    nff: __queryNF,
    nfn: (res, db: any, _, p, __, opts) => {
        for (let s in db) {
            __querySP(res, db[s], s, p, null, opts);
        }
    },
    nnl: __queryNN,
    nnf: __queryNN,
    nnn: (res, db) => Object.assign(res, db),
};

/**
 * Query function implementation, dispatches to one of the 27 optimized
 * functions based on given query pattern.
 *
 * @internal
 */
const __impl = defmulti<
    QueryObj,
    QueryObj,
    SPTerm,
    SPTerm,
    OTerm,
    QueryOpts,
    void
>((_, __, s, p, o) => __classify(s) + __classify(p) + __classify(o), {}, IMPLS);

/** @internal */
const __objQuery = (src: QueryObj[], opts: QueryOpts, args: any[]) => {
    const isIsec = opts.cwise && opts.intersect;
    isIsec && (opts.cwise = false);
    let [s, p, o, out] = <[SPInputTerm, SPInputTerm, OTerm, QueryObj?]>args;
    out = out || {};
    __impl(
        out,
        src,
        __coerceStr(s),
        __coerceStr(p),
        __coerce(o, isIsec),
        <QueryOpts>opts
    );
    return out;
};

/** @internal */
const __arrayQuery = (
    src: QueryObj[],
    opts: QueryOpts,
    p: SPInputTerm,
    o: OTerm,
    collect: Fn2<any, number, void>
) => {
    const isIsec = opts.cwise && opts.intersect;
    isIsec && (opts.cwise = false);
    const $p = __coerceStr(p);
    const $o = __coerce(o, isIsec);
    // pre-select implementation to avoid dynamic dispatch
    const impl = IMPLS[<QueryType>("n" + __classify($p) + __classify($o))];
    for (let i = 0, n = src.length; i < n; i++) {
        const res: QueryObj = {};
        impl(res, { _: src[i] }, "_", $p, $o, opts);
        res._ && collect(res._, i);
    }
};

/** @internal */
const DEFAULT_OPTS: QueryOpts = {
    partial: false,
    cwise: true,
    intersect: false,
    equiv,
};

/**
 * Generic Higher-order function to return an actual query function based on
 * given behavior options.
 *
 * @remarks
 * @see {@link QueryOpts}
 * @see {@link ObjQueryFn}
 * @see {@link ArrayQueryFn}
 * @see {@link defKeyQuery}
 *
 * @param opts -
 */
export const defQuery = <T extends QueryObj | QueryObj[] = QueryObj>(
    opts?: Partial<QueryOpts>
): QueryFn<T> => {
    const $opts: QueryOpts = { ...DEFAULT_OPTS, ...opts };
    return <QueryFn<T>>((src: any, ...args: any[]): any => {
        if (isArray(src)) {
            const out: QueryObj[] = args[2] || [];
            __arrayQuery(src, $opts, args[0], args[1], (x) => out.push(x));
            return out;
        } else {
            return __objQuery(src, $opts, args);
        }
    });
};

/**
 * Generic Higher-order function to return an actual query function based on
 * given behavior options. Unlike {@link defQuery}, key query functions only
 * return sets of keys (or indices) of matching objects.
 *
 * @remarks
 * @see {@link KeyQueryOpts}
 * @see {@link ObjKeyQueryFn}
 * @see {@link ArrayKeyQueryFn}
 *
 * @param opts -
 */
export const defKeyQuery = <T extends QueryObj | QueryObj[] = QueryObj>(
    opts?: Partial<KeyQueryOpts>
) => {
    const $opts: QueryOpts = { ...DEFAULT_OPTS, ...opts };
    return <KeyQueryFn<T>>((src: any, ...args: any[]): any => {
        if (isArray(src)) {
            const out = args[2] || new Set<number>();
            __arrayQuery(src, $opts, args[0], args[1], (_, i) => out.add(i));
            return out;
        } else {
            const res = __objQuery(src, $opts, args.slice(0, 3));
            const out = args[3];
            if (!out) return new Set<string>(Object.keys(res));
            for (let k in res) out.add(k);
            return out;
        }
    });
};

/**
 * Multi-term query function for collections (arrays) of {@link QueryObj}ects.
 * Takes a number of {@link QueryTerm}s and matches each term in succession
 * against the array of results of the previous terms (i.e. each sub-query is
 * potentially further narrowing the result set). Returns final results,
 * possibly post-processed, depending on given options.
 *
 * @remarks
 * Each {@link QueryTerm} can provide its own options and post-processing
 * function. Furthermore, global post-processing (e.g. limiting number of final
 * results, sorting by key) can be configured via `opts`.
 *
 * @param db
 * @param terms
 * @param opts
 */
export const query = <T extends QueryObj = QueryObj>(
    db: T[],
    terms: Nullable<QueryTerm<T>>[],
    opts: Partial<MultiQueryOpts<T>> = {}
) => {
    for (let term of terms) {
        if (!term) continue;
        db = <T[]>(
            (<ArrayQueryFn<T[]>>defQuery<T[]>(term.opts))(
                db,
                <string>term.q[0],
                term.q[1]
            )
        );
        term.post && (db = term.post(db));
        if (!db.length) return db;
    }
    const limit = opts.limit || 0;
    if (limit > 0 && limit < db.length) {
        db.length = limit;
    }
    if (opts.sort) {
        db.sort(
            compareByKey<T, any>(
                opts.sort,
                opts.reverse ? reverse(compare) : compare
            )
        );
    }
    return db;
};