@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 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: 5.78 KB
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.10.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.14",
44
- "@thi.ng/bidir-index": "^1.5.0",
45
- "@thi.ng/checks": "^3.8.4",
46
- "@thi.ng/errors": "^2.6.3",
47
- "@thi.ng/rle-pack": "^3.2.1"
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": "f15a589f1695f67f2e90b8d221a89fc9960a2e83\n"
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<import("./api.js").RowWithMeta<T>, void, unknown>;
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
- for (let i of ctx) yield table.getRow(i, false, true);
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) mask.set(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
- if (mask) {
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
- if (mask) {
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) return false;
207
- colBitmaps.push(b);
208
- }
209
- for (let b of colBitmaps) {
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
- if (mask) {
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
- return false;
290
+ if (isNeg) {
291
+ if (mask) mask.fill(0);
292
+ else mask = ctx.makeMask();
293
+ } else return false;
245
294
  }
246
295
  }
247
- if (mask) {
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 { __clamp } from "./internal/indexof.js";
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
- const max = this.length;
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;