@thi.ng/column-store 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/package.json +8 -7
- package/query.d.ts +20 -4
- package/query.js +85 -30
- package/table.d.ts +11 -0
- package/table.js +19 -4
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
- [Predicate-based matchers](#predicate-based-matchers)
|
|
40
40
|
- [Row ranges](#row-ranges)
|
|
41
41
|
- [Value ranges](#value-ranges)
|
|
42
|
+
- [Result order and pagination](#result-order-and-pagination)
|
|
42
43
|
- [Custom operators](#custom-operators)
|
|
43
44
|
- [Result aggregation](#result-aggregation)
|
|
44
45
|
- [Query ranges](#query-ranges)
|
|
@@ -450,6 +451,15 @@ operator selects rows based on a given column's `start` .. `end` vaulue range
|
|
|
450
451
|
query.valueRange("id", 100, 109);
|
|
451
452
|
```
|
|
452
453
|
|
|
454
|
+
### Result order and pagination
|
|
455
|
+
|
|
456
|
+
- [`sortBy()`](https://docs.thi.ng/umbrella/column-store/classes/Query.html#sortby)
|
|
457
|
+
allows query results to be ordered (ascending or descending) via an arbitrary
|
|
458
|
+
number of sort columns or criteria, applied in the given order.
|
|
459
|
+
- [`limit()`](https://docs.thi.ng/umbrella/column-store/classes/Query.html#limit)
|
|
460
|
+
provides basic pagination of query results by specifying a max. number of
|
|
461
|
+
results and start offset
|
|
462
|
+
|
|
453
463
|
### Custom operators
|
|
454
464
|
|
|
455
465
|
Custom query operators can be registered via
|
|
@@ -495,13 +505,14 @@ For Node.js REPL:
|
|
|
495
505
|
const cs = await import("@thi.ng/column-store");
|
|
496
506
|
```
|
|
497
507
|
|
|
498
|
-
Package sizes (brotli'd, pre-treeshake): ESM:
|
|
508
|
+
Package sizes (brotli'd, pre-treeshake): ESM: 6.28 KB
|
|
499
509
|
|
|
500
510
|
## Dependencies
|
|
501
511
|
|
|
502
512
|
- [@thi.ng/api](https://github.com/thi-ng/umbrella/tree/develop/packages/api)
|
|
503
513
|
- [@thi.ng/bidir-index](https://github.com/thi-ng/umbrella/tree/develop/packages/bidir-index)
|
|
504
514
|
- [@thi.ng/checks](https://github.com/thi-ng/umbrella/tree/develop/packages/checks)
|
|
515
|
+
- [@thi.ng/compare](https://github.com/thi-ng/umbrella/tree/develop/packages/compare)
|
|
505
516
|
- [@thi.ng/errors](https://github.com/thi-ng/umbrella/tree/develop/packages/errors)
|
|
506
517
|
- [@thi.ng/rle-pack](https://github.com/thi-ng/umbrella/tree/develop/packages/rle-pack)
|
|
507
518
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thi.ng/column-store",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "In-memory column store database with customizable column types, extensible query engine, bitfield indexing for query acceleration, JSON serialization with optional RLE compression",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -40,11 +40,12 @@
|
|
|
40
40
|
"tool:tangle": "../../node_modules/.bin/tangle src/**/*.ts"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@thi.ng/api": "^8.12.
|
|
44
|
-
"@thi.ng/bidir-index": "^1.5.
|
|
45
|
-
"@thi.ng/checks": "^3.8.
|
|
46
|
-
"@thi.ng/
|
|
47
|
-
"@thi.ng/
|
|
43
|
+
"@thi.ng/api": "^8.12.15",
|
|
44
|
+
"@thi.ng/bidir-index": "^1.5.1",
|
|
45
|
+
"@thi.ng/checks": "^3.8.5",
|
|
46
|
+
"@thi.ng/compare": "^2.5.1",
|
|
47
|
+
"@thi.ng/errors": "^2.6.4",
|
|
48
|
+
"@thi.ng/rle-pack": "^3.2.2"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"esbuild": "^0.27.2",
|
|
@@ -129,5 +130,5 @@
|
|
|
129
130
|
"status": "alpha",
|
|
130
131
|
"year": 2025
|
|
131
132
|
},
|
|
132
|
-
"gitHead": "
|
|
133
|
+
"gitHead": "8f50352caab9ec7757d645c0afa605dfb5427abe\n"
|
|
133
134
|
}
|
package/query.d.ts
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
|
-
import type { Predicate } from "@thi.ng/api";
|
|
2
|
-
import type { ColumnID, QueryTerm, QueryTermOpSpec, Row } from "./api.js";
|
|
1
|
+
import type { Comparator, Predicate } from "@thi.ng/api";
|
|
2
|
+
import type { ColumnID, QueryTerm, QueryTermOpSpec, Row, RowWithMeta } from "./api.js";
|
|
3
3
|
import type { Table } from "./table.js";
|
|
4
4
|
export declare class Query<T extends Row> {
|
|
5
5
|
readonly table: Table<T>;
|
|
6
6
|
terms: QueryTerm<T>[];
|
|
7
|
+
protected _cmp?: Comparator<any>;
|
|
8
|
+
protected _limit: number;
|
|
9
|
+
protected _offset: number;
|
|
7
10
|
constructor(table: Table<T>, terms?: QueryTerm<T>[]);
|
|
8
11
|
addTerm(term: QueryTerm<T>): this;
|
|
12
|
+
limit(limit: number, offset?: number): void;
|
|
13
|
+
/**
|
|
14
|
+
* Constructs a comparator for query results based on given sort criteria,
|
|
15
|
+
* which are applied in given order. Each criteria can be on of:
|
|
16
|
+
*
|
|
17
|
+
* - comparator (applied to full rows)
|
|
18
|
+
* - columnID
|
|
19
|
+
* - tuple of `[columnID, boolean]` (if boolean is true, sorts in ascending order)
|
|
20
|
+
* - tuple of `[columnID, comparator]`
|
|
21
|
+
*
|
|
22
|
+
* @param order
|
|
23
|
+
*/
|
|
24
|
+
sortBy(...order: (ColumnID<T> | Comparator<T> | [ColumnID<T>, boolean | Comparator<any>])[]): this;
|
|
9
25
|
/** Alias for {@link Query.or} */
|
|
10
26
|
where: (column: ColumnID<T>, value: any) => this;
|
|
11
27
|
/** Alias for {@link Query.nor} */
|
|
@@ -19,7 +35,7 @@ export declare class Query<T extends Row> {
|
|
|
19
35
|
matchRow(pred: Predicate<T>): this;
|
|
20
36
|
valueRange(column: ColumnID<T>, start: any, end?: any): this;
|
|
21
37
|
rowRange(start?: number, end?: number): this;
|
|
22
|
-
[Symbol.iterator](): Generator<
|
|
38
|
+
[Symbol.iterator](): Generator<RowWithMeta<T>, void, unknown>;
|
|
23
39
|
}
|
|
24
40
|
export declare class QueryCtx<T extends Row> {
|
|
25
41
|
readonly query: Query<T>;
|
|
@@ -33,7 +49,7 @@ export declare class QueryCtx<T extends Row> {
|
|
|
33
49
|
*/
|
|
34
50
|
[Symbol.iterator](): Generator<number, void, unknown>;
|
|
35
51
|
clear(): void;
|
|
36
|
-
makeMask(seed?: Uint32Array): Uint32Array<ArrayBuffer>;
|
|
52
|
+
makeMask(seed?: number | Uint32Array): Uint32Array<ArrayBuffer>;
|
|
37
53
|
/**
|
|
38
54
|
* Combines the `mask` with the context's mask (combined using bitwise AND).
|
|
39
55
|
* If the context mask is still undefined, assigns `mask` as the initial
|
package/query.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { isArray } from "@thi.ng/checks/is-array";
|
|
2
|
+
import { isFunction } from "@thi.ng/checks/is-function";
|
|
3
|
+
import { isNumber } from "@thi.ng/checks/is-number";
|
|
4
|
+
import { isString } from "@thi.ng/checks/is-string";
|
|
5
|
+
import { compare } from "@thi.ng/compare/compare";
|
|
6
|
+
import { composeComparators } from "@thi.ng/compare/compose";
|
|
7
|
+
import { compareByKey } from "@thi.ng/compare/keys";
|
|
8
|
+
import { reverse } from "@thi.ng/compare/reverse";
|
|
2
9
|
import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
|
|
3
10
|
import { unsupportedOp } from "@thi.ng/errors/unsupported";
|
|
4
11
|
import { Bitfield } from "./bitmap.js";
|
|
@@ -10,11 +17,39 @@ class Query {
|
|
|
10
17
|
for (let term of terms) this.addTerm(term);
|
|
11
18
|
}
|
|
12
19
|
terms = [];
|
|
20
|
+
_cmp;
|
|
21
|
+
_limit = Infinity;
|
|
22
|
+
_offset = 0;
|
|
13
23
|
addTerm(term) {
|
|
14
24
|
if (!QUERY_OPS[term.type]) unsupportedOp(`query type: ${term.type}`);
|
|
15
25
|
this.terms.push(term);
|
|
16
26
|
return this;
|
|
17
27
|
}
|
|
28
|
+
limit(limit, offset = 0) {
|
|
29
|
+
this._limit = limit;
|
|
30
|
+
this._offset = offset;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Constructs a comparator for query results based on given sort criteria,
|
|
34
|
+
* which are applied in given order. Each criteria can be on of:
|
|
35
|
+
*
|
|
36
|
+
* - comparator (applied to full rows)
|
|
37
|
+
* - columnID
|
|
38
|
+
* - tuple of `[columnID, boolean]` (if boolean is true, sorts in ascending order)
|
|
39
|
+
* - tuple of `[columnID, comparator]`
|
|
40
|
+
*
|
|
41
|
+
* @param order
|
|
42
|
+
*/
|
|
43
|
+
sortBy(...order) {
|
|
44
|
+
const fns = order.map(
|
|
45
|
+
(x) => isFunction(x) ? x : isString(x) ? compareByKey(x) : compareByKey(
|
|
46
|
+
x[0],
|
|
47
|
+
isFunction(x[1]) ? x[1] : x[1] ? compare : reverse(compare)
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
this._cmp = composeComparators(...fns);
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
18
53
|
/** Alias for {@link Query.or} */
|
|
19
54
|
where = (column, value) => this.or(column, value);
|
|
20
55
|
/** Alias for {@link Query.nor} */
|
|
@@ -65,7 +100,7 @@ class Query {
|
|
|
65
100
|
return this;
|
|
66
101
|
}
|
|
67
102
|
*[Symbol.iterator]() {
|
|
68
|
-
const { table } = this;
|
|
103
|
+
const { table, _limit, _offset } = this;
|
|
69
104
|
const ctx = new QueryCtx(this);
|
|
70
105
|
for (let term of this.terms) {
|
|
71
106
|
const op = QUERY_OPS[term.type];
|
|
@@ -80,8 +115,25 @@ class Query {
|
|
|
80
115
|
}
|
|
81
116
|
if (!op.fn(ctx, term, column)) return;
|
|
82
117
|
}
|
|
83
|
-
if (ctx.bitmap)
|
|
84
|
-
|
|
118
|
+
if (!ctx.bitmap) return;
|
|
119
|
+
if (this._cmp) {
|
|
120
|
+
const rows = [];
|
|
121
|
+
for (let i of ctx) {
|
|
122
|
+
rows.push(table.getRow(i, false, true));
|
|
123
|
+
}
|
|
124
|
+
rows.sort(this._cmp);
|
|
125
|
+
for (let i = this._offset, n = Math.min(rows.length, i + this._limit); i < n; i++) {
|
|
126
|
+
yield rows[i];
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
let j = 0;
|
|
131
|
+
for (let i of ctx) {
|
|
132
|
+
if (j >= _offset) {
|
|
133
|
+
if (j >= _limit) return;
|
|
134
|
+
yield table.getRow(i, false, true);
|
|
135
|
+
}
|
|
136
|
+
j++;
|
|
85
137
|
}
|
|
86
138
|
}
|
|
87
139
|
}
|
|
@@ -111,7 +163,9 @@ class QueryCtx {
|
|
|
111
163
|
}
|
|
112
164
|
makeMask(seed) {
|
|
113
165
|
const mask = new Uint32Array(this.size);
|
|
114
|
-
if (seed)
|
|
166
|
+
if (seed) {
|
|
167
|
+
isNumber(seed) ? mask.fill(seed) : mask.set(seed);
|
|
168
|
+
}
|
|
115
169
|
return mask;
|
|
116
170
|
}
|
|
117
171
|
/**
|
|
@@ -166,11 +220,7 @@ const execBitOr = (ctx, term, column) => {
|
|
|
166
220
|
const b = bitmap.index.get(key)?.buffer;
|
|
167
221
|
if (b) mask = ctx.makeMask(b);
|
|
168
222
|
}
|
|
169
|
-
|
|
170
|
-
term.type === "nor" ? ctx.mergeInvMask(mask) : ctx.mergeMask(mask);
|
|
171
|
-
return true;
|
|
172
|
-
}
|
|
173
|
-
return false;
|
|
223
|
+
return __finalizeMask(ctx, mask, term.type === "nor");
|
|
174
224
|
};
|
|
175
225
|
const execOr = (ctx, term, column) => {
|
|
176
226
|
const key = column.valueKey(term.value);
|
|
@@ -184,11 +234,7 @@ const execOr = (ctx, term, column) => {
|
|
|
184
234
|
}
|
|
185
235
|
}
|
|
186
236
|
}
|
|
187
|
-
|
|
188
|
-
term.type === "nor" ? ctx.mergeInvMask(mask) : ctx.mergeMask(mask);
|
|
189
|
-
return true;
|
|
190
|
-
}
|
|
191
|
-
return false;
|
|
237
|
+
return __finalizeMask(ctx, mask, term.type === "nor");
|
|
192
238
|
};
|
|
193
239
|
const delegateOr = (ctx, term, column) => (term.value != null && column.bitmap ? execBitOr : execOr)(
|
|
194
240
|
ctx,
|
|
@@ -198,15 +244,18 @@ const delegateOr = (ctx, term, column) => (term.value != null && column.bitmap ?
|
|
|
198
244
|
const execBitAnd = (ctx, term, column) => {
|
|
199
245
|
const bitmap = column.bitmap;
|
|
200
246
|
const key = column.valueKey(term.value);
|
|
247
|
+
const isNeg = term.type === "nand";
|
|
201
248
|
let mask;
|
|
202
249
|
if (isArray(key)) {
|
|
203
|
-
const colBitmaps = [];
|
|
204
250
|
for (let k of key) {
|
|
205
251
|
const b = bitmap.index.get(k)?.buffer;
|
|
206
|
-
if (!b)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
252
|
+
if (!b) {
|
|
253
|
+
if (isNeg) {
|
|
254
|
+
if (mask) mask.fill(0);
|
|
255
|
+
else mask = ctx.makeMask();
|
|
256
|
+
continue;
|
|
257
|
+
} else return false;
|
|
258
|
+
}
|
|
210
259
|
if (mask) {
|
|
211
260
|
const n = b.length;
|
|
212
261
|
for (let i = 0; i < n; i++) mask[i] &= b[i];
|
|
@@ -217,16 +266,13 @@ const execBitAnd = (ctx, term, column) => {
|
|
|
217
266
|
const b = bitmap.index.get(key)?.buffer;
|
|
218
267
|
if (b) mask = ctx.makeMask(b);
|
|
219
268
|
}
|
|
220
|
-
|
|
221
|
-
term.type === "nand" ? ctx.mergeInvMask(mask) : ctx.mergeMask(mask);
|
|
222
|
-
return true;
|
|
223
|
-
}
|
|
224
|
-
return false;
|
|
269
|
+
return __finalizeMask(ctx, mask, isNeg);
|
|
225
270
|
};
|
|
226
271
|
const execAnd = (ctx, term, column) => {
|
|
227
272
|
const n = ctx.table.length;
|
|
228
273
|
const key = column.valueKey(term.value) ?? null;
|
|
229
274
|
const pred = column.isArray ? (row, v) => row.includes(v) : (row, v) => row === v;
|
|
275
|
+
const isNeg = term.type === "nand";
|
|
230
276
|
let mask;
|
|
231
277
|
for (let k of isArray(key) ? key : [key]) {
|
|
232
278
|
let m;
|
|
@@ -241,20 +287,29 @@ const execAnd = (ctx, term, column) => {
|
|
|
241
287
|
for (let i = 0; i < n; i++) mask[i] &= m[i];
|
|
242
288
|
} else mask = m;
|
|
243
289
|
} else {
|
|
244
|
-
|
|
290
|
+
if (isNeg) {
|
|
291
|
+
if (mask) mask.fill(0);
|
|
292
|
+
else mask = ctx.makeMask();
|
|
293
|
+
} else return false;
|
|
245
294
|
}
|
|
246
295
|
}
|
|
247
|
-
|
|
248
|
-
term.type === "nand" ? ctx.mergeInvMask(mask) : ctx.mergeMask(mask);
|
|
249
|
-
return true;
|
|
250
|
-
}
|
|
251
|
-
return false;
|
|
296
|
+
return __finalizeMask(ctx, mask, isNeg);
|
|
252
297
|
};
|
|
253
298
|
const delegateAnd = (ctx, term, column) => (term.value != null && column.bitmap ? execBitAnd : execAnd)(
|
|
254
299
|
ctx,
|
|
255
300
|
term,
|
|
256
301
|
column
|
|
257
302
|
);
|
|
303
|
+
const __finalizeMask = (ctx, mask, isNeg) => {
|
|
304
|
+
if (mask) {
|
|
305
|
+
isNeg ? ctx.mergeInvMask(mask) : ctx.mergeMask(mask);
|
|
306
|
+
return true;
|
|
307
|
+
} else if (isNeg) {
|
|
308
|
+
if (!ctx.bitmap) ctx.bitmap = ctx.makeMask(-1);
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
};
|
|
258
313
|
const QUERY_OPS = {
|
|
259
314
|
or: { fn: delegateOr },
|
|
260
315
|
nor: { fn: delegateOr },
|
package/table.d.ts
CHANGED
|
@@ -35,6 +35,17 @@ export declare class Table<T extends Row> implements IClear, ICopy<Table<T>>, IE
|
|
|
35
35
|
getPartialRow<K extends ColumnID<T>>(i: number, columns: K[], safe?: boolean): Maybe<Pick<T, K>>;
|
|
36
36
|
getPartialRow<K extends ColumnID<T>>(i: number, columns: K[], safe?: boolean, includeID?: false): Maybe<Pick<T, K>>;
|
|
37
37
|
getPartialRow<K extends ColumnID<T>>(i: number, columns: K[], safe?: boolean, includeID?: true): Maybe<RowWithMeta<Pick<T, K>>>;
|
|
38
|
+
getRows(start?: number, end?: number, includeID?: false): IterableIterator<T>;
|
|
39
|
+
getRows(start?: number, end?: number, includeID?: true): IterableIterator<RowWithMeta<T>>;
|
|
40
|
+
getPartialRows<K extends ColumnID<T>>(columns: K[], start?: number, end?: number, includeID?: false): IterableIterator<Pick<T, K>>;
|
|
41
|
+
getPartialRows<K extends ColumnID<T>>(columns: K[], start?: number, end?: number, includeID?: true): IterableIterator<RowWithMeta<Pick<T, K>>>;
|
|
42
|
+
/**
|
|
43
|
+
* Creates a new table with same schema and options, but only containing the
|
|
44
|
+
* subset of rows in the `[start,end)` interval.
|
|
45
|
+
*
|
|
46
|
+
* @param start
|
|
47
|
+
* @param end
|
|
48
|
+
*/
|
|
38
49
|
slice(start?: number, end?: number): Table<T>;
|
|
39
50
|
indexOf(id: ColumnID<T>, value: any, start?: number, end?: number): number;
|
|
40
51
|
lastIndexOf(id: ColumnID<T>, value: any, start?: number, end?: number): number;
|
package/table.js
CHANGED
|
@@ -13,7 +13,7 @@ import { TupleColumn } from "./columns/tuple.js";
|
|
|
13
13
|
import { TypedArrayColumn } from "./columns/typedarray.js";
|
|
14
14
|
import { VectorColumn } from "./columns/vector.js";
|
|
15
15
|
import { __columnError } from "./internal/checks.js";
|
|
16
|
-
import {
|
|
16
|
+
import { __clampRange } from "./internal/indexof.js";
|
|
17
17
|
import { Query } from "./query.js";
|
|
18
18
|
class Table {
|
|
19
19
|
opts;
|
|
@@ -118,10 +118,25 @@ class Table {
|
|
|
118
118
|
}
|
|
119
119
|
return row;
|
|
120
120
|
}
|
|
121
|
+
*getRows(start = 0, end, includeID = false) {
|
|
122
|
+
[start, end] = __clampRange(this.length, start, end);
|
|
123
|
+
for (let i = start; i < end; i++)
|
|
124
|
+
yield this.getRow(i, false, includeID);
|
|
125
|
+
}
|
|
126
|
+
*getPartialRows(columns, start = 0, end, includeID = false) {
|
|
127
|
+
[start, end] = __clampRange(this.length, start, end);
|
|
128
|
+
for (let i = start; i < end; i++)
|
|
129
|
+
yield this.getPartialRow(i, columns, false, includeID);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Creates a new table with same schema and options, but only containing the
|
|
133
|
+
* subset of rows in the `[start,end)` interval.
|
|
134
|
+
*
|
|
135
|
+
* @param start
|
|
136
|
+
* @param end
|
|
137
|
+
*/
|
|
121
138
|
slice(start = 0, end) {
|
|
122
|
-
|
|
123
|
-
start = __clamp(start, 0, max);
|
|
124
|
-
end = __clamp(end ?? max, start, max);
|
|
139
|
+
[start, end] = __clampRange(this.length, start, end);
|
|
125
140
|
const copy = this.empty();
|
|
126
141
|
for (let i = start; i < end; i++) copy.addRow(this.getRow(i, true));
|
|
127
142
|
return copy;
|