@thi.ng/column-store 0.4.1 → 0.6.0

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
@@ -34,6 +34,8 @@
34
34
  - [AND](#and)
35
35
  - [Negation](#negation)
36
36
  - [Predicate-based matchers](#predicate-based-matchers)
37
+ - [Row ranges](#row-ranges)
38
+ - [Value ranges](#value-ranges)
37
39
  - [Custom operators](#custom-operators)
38
40
  - [Result aggregation](#result-aggregation)
39
41
  - [Query ranges](#query-ranges)
@@ -55,10 +57,11 @@ As the name indicates, data is stored in different columns, where each column
55
57
  manages its own schema, value validation, backing storage, indexing,
56
58
  serialization etc.
57
59
 
58
- From a user's POV, columns are not (usually) used directly, but via table, which
59
- acts as facade to the various configured columns. Data items are added as JS
60
- objects to a table, which then pulls out related values, validates them and the
61
- delegates them to the columns.
60
+ From a user's POV, columns are not (usually) used directly, but accessed via a
61
+ table, which provides row-based access to compound data records and acts as
62
+ facade to the various configured columns. Data records are added as JS objects
63
+ to a table, which then pulls out related values, validates them and the
64
+ delegates them to the individual columns for encoding & storage.
62
65
 
63
66
  An example table definition looks like this (explanation of column types in next
64
67
  section below):
@@ -66,8 +69,17 @@ section below):
66
69
  ```ts tangle:export/readme-types.ts
67
70
  import { Table, FLAG_DICT, FLAG_UNIQUE } from "@thi.ng/column-store";
68
71
 
72
+ // schema for a single row item
73
+ interface Item {
74
+ id: number;
75
+ name: string;
76
+ aliases?: string[];
77
+ latlon: number[] | Float32Array;
78
+ tags: string[];
79
+ }
80
+
69
81
  // define a table with the given columns
70
- const table = new Table({
82
+ const table = new Table<Item>({
71
83
 
72
84
  // column of single numeric values
73
85
  // (default cardinality makes values required)
@@ -367,6 +379,30 @@ can be used, otherwise the behavior is:
367
379
  - [`matchPartialRow`](https://docs.thi.ng/umbrella/column-store/classes/Query.html#matchpartialrow):
368
380
  apply predicate to partial row (only selected columns)
369
381
 
382
+ #### Row ranges
383
+
384
+ The
385
+ [`rowRange`](https://docs.thi.ng/umbrella/column-store/classes/Query.html#rowrange)
386
+ operator selects the given `start` (inclusive) .. `end` (exclusive) range of row
387
+ indices.
388
+
389
+ ```ts
390
+ // select first ten rows
391
+ query.rowRange(0, 10);
392
+ ```
393
+
394
+ #### Value ranges
395
+
396
+ The
397
+ [`valueRange`](https://docs.thi.ng/umbrella/column-store/classes/Query.html#valuerange)
398
+ operator selects rows based on a given column's `start` .. `end` vaulue range
399
+ (both inclusive) of row indices.
400
+
401
+ ```ts
402
+ // select rows where ID is in closed [100,109] interval
403
+ query.valueRange("id", 100, 109);
404
+ ```
405
+
370
406
  ### Custom operators
371
407
 
372
408
  Custom query operators can be registered via
@@ -412,7 +448,7 @@ For Node.js REPL:
412
448
  const cs = await import("@thi.ng/column-store");
413
449
  ```
414
450
 
415
- Package sizes (brotli'd, pre-treeshake): ESM: 4.67 KB
451
+ Package sizes (brotli'd, pre-treeshake): ESM: 5.50 KB
416
452
 
417
453
  ## Dependencies
418
454
 
@@ -438,7 +474,13 @@ import {
438
474
  FLAG_UNIQUE,
439
475
  } from "@thi.ng/column-store";
440
476
 
441
- const table = new Table({
477
+ interface Item {
478
+ id: number;
479
+ type: string;
480
+ tags: string[];
481
+ }
482
+
483
+ const table = new Table<Item>({
442
484
  // ID column stores 8bit ints (typed array)
443
485
  id: { type: "u8" },
444
486
  // Type column stores indexed strings
package/api.d.ts CHANGED
@@ -1,8 +1,9 @@
1
- import type { FloatType, Fn3, IntType, Maybe, UintType } from "@thi.ng/api";
1
+ import type { FloatType, Fn3, IntType, Maybe, Predicate, UintType } from "@thi.ng/api";
2
2
  import type { BitmapIndex } from "./bitmap.js";
3
3
  import type { QueryCtx } from "./query.js";
4
4
  import type { Table } from "./table.js";
5
- export type ColumnSchema = Record<string, ColumnSpec>;
5
+ export type ColumnID<T extends Row> = Exclude<keyof T, number | symbol>;
6
+ export type ColumnSchema<T extends Row> = Record<ColumnID<T>, ColumnSpec>;
6
7
  export type NumericType = IntType | UintType | FloatType;
7
8
  export type VectorType = `${NumericType}vec`;
8
9
  export type Cardinality = [number, number];
@@ -55,7 +56,7 @@ export interface ColumnTypeSpec {
55
56
  * Factory function to instantiate a colum type for given table, column name
56
57
  * & spec.
57
58
  */
58
- impl: Fn3<Table, string, ColumnSpec, IColumn>;
59
+ impl: Fn3<Table<any>, string, ColumnSpec, IColumn>;
59
60
  /**
60
61
  * Bit mask of supported flags (default: 0, i.e. none allowed). During
61
62
  * column validation, the (inverted) mask will be applied to the user
@@ -91,14 +92,45 @@ export interface IColumn {
91
92
  validate(value: any): boolean;
92
93
  /**
93
94
  * Searches for `value` in the column data, optionally constrained to given
94
- * `start`/`end` range. If found, returns row ID of first occurrence,
95
- * otherwise -1.
95
+ * `start`/`end` range (end index is exclusive and defaults to current table
96
+ * length). If found, returns row ID of first occurrence, otherwise -1.
96
97
  *
97
98
  * @param value
98
99
  * @param start
99
100
  * @param end
100
101
  */
101
102
  indexOf(value: any, start?: number, end?: number): number;
103
+ /**
104
+ * Searches for `value` in the column data in reverse order (from the last
105
+ * row), optionally constrained to given `start`/`end` range (end index is
106
+ * exclusive and defaults to current table length). If found, returns row ID
107
+ * of first occurrence, otherwise -1.
108
+ *
109
+ * @param value
110
+ * @param start
111
+ * @param end
112
+ */
113
+ lastIndexOf(value: any, start?: number, end?: number): number;
114
+ /**
115
+ * Similar to {@link IColumn.indexOf}, but applies given predicate function
116
+ * to each row value. Returns index of first row for which the predicate is
117
+ * truthy, otherwise returns -1.
118
+ *
119
+ * @param pred
120
+ * @param start
121
+ * @param end
122
+ */
123
+ findIndex(pred: Predicate<any>, start?: number, end?: number): number;
124
+ /**
125
+ * Similar to {@link IColumn.lastIndexOf}, but applies given predicate
126
+ * function to each row value. Returns index of first row for which the
127
+ * predicate is truthy, otherwise returns -1.
128
+ *
129
+ * @param pred
130
+ * @param start
131
+ * @param end
132
+ */
133
+ findLastIndex(pred: Predicate<any>, start?: number, end?: number): number;
102
134
  /**
103
135
  * Returns value at given row.
104
136
  *
@@ -123,9 +155,9 @@ export interface IColumn {
123
155
  getRowKey(i: number): any;
124
156
  valueKey(value: any): any;
125
157
  }
126
- export interface SerializedTable {
127
- schema: ColumnSchema;
128
- columns: Record<string, SerializedColumn>;
158
+ export interface SerializedTable<T extends Row> {
159
+ schema: ColumnSchema<T>;
160
+ columns: Record<ColumnID<T>, SerializedColumn>;
129
161
  length: number;
130
162
  }
131
163
  export interface SerializedColumn {
@@ -137,13 +169,16 @@ export interface SerializedIndex {
137
169
  next: number;
138
170
  }
139
171
  export type Row = Record<string, any>;
140
- export interface QueryTerm {
172
+ export type RowWithMeta<T extends Row> = T & {
173
+ __row: number;
174
+ };
175
+ export interface QueryTerm<T extends Row> {
141
176
  type: string;
142
- column?: string;
177
+ column?: ColumnID<T>;
143
178
  value: any;
144
179
  params?: any;
145
180
  }
146
- export type QueryTermOp = Fn3<QueryCtx, QueryTerm, Maybe<IColumn>, void>;
181
+ export type QueryTermOp = Fn3<QueryCtx<any>, QueryTerm<any>, Maybe<IColumn>, void>;
147
182
  export interface QueryTermOpSpec {
148
183
  /**
149
184
  * Default mode is: "col";
package/bitmap.d.ts CHANGED
@@ -28,8 +28,10 @@ export declare class Bitfield {
28
28
  constructor(buffer?: Uint32Array | undefined);
29
29
  ones(max?: number): Generator<number, void, unknown>;
30
30
  first(start?: number, end?: number): number;
31
+ last(start?: number, end?: number): number;
31
32
  setBit(id: number): void;
32
33
  clearBit(id: number): void;
34
+ fill(x: 0 | 1, start: number, end: number): this;
33
35
  ensure(size: number): Uint32Array<ArrayBufferLike>;
34
36
  removeBit(id: number): void;
35
37
  }
package/bitmap.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { __clampRange } from "./internal/indexof.js";
1
2
  class BitmapIndex {
2
3
  index = /* @__PURE__ */ new Map();
3
4
  /**
@@ -62,9 +63,11 @@ class Bitfield {
62
63
  }
63
64
  }
64
65
  }
65
- first(start = 0, end = (this.buffer?.length ?? -1) * 32) {
66
+ first(start = 0, end) {
66
67
  const { buffer } = this;
67
- if (!buffer || start >= end) return -1;
68
+ if (!buffer) return -1;
69
+ [start, end] = __clampRange(buffer.length << 5, start, end);
70
+ if (start >= end) return -1;
68
71
  for (let i = start >>> 5, n = Math.min(Math.ceil(end / 32), buffer.length); i < n; i++) {
69
72
  let bits = buffer[i];
70
73
  while (bits) {
@@ -76,6 +79,22 @@ class Bitfield {
76
79
  }
77
80
  return -1;
78
81
  }
82
+ last(start = 0, end) {
83
+ const { buffer } = this;
84
+ if (!buffer) return -1;
85
+ [start, end] = __clampRange(buffer.length << 5, start, end);
86
+ if (start >= end) return -1;
87
+ for (let i = end >>> 5, n = start >>> 5; i >= n; i--) {
88
+ let bits = buffer[i];
89
+ while (bits) {
90
+ const msb = Math.clz32(bits) ^ 31;
91
+ const x = (i << 5) + msb;
92
+ if (x >= start && x < end) return x;
93
+ bits ^= 1 << msb;
94
+ }
95
+ }
96
+ return -1;
97
+ }
79
98
  setBit(id) {
80
99
  const w = id >>> 5;
81
100
  this.ensure(w)[w] |= 1 << (id & 31);
@@ -84,6 +103,22 @@ class Bitfield {
84
103
  const w = id >>> 5;
85
104
  this.ensure(w)[w] &= ~(1 << (id & 31));
86
105
  }
106
+ fill(x, start, end) {
107
+ const i = start >>> 5;
108
+ const j = end >>> 5;
109
+ let m = ~((1 << (start & 31)) - 1);
110
+ const buf = this.ensure(j);
111
+ if (i === j) {
112
+ m &= (1 << (end & 31)) - 1;
113
+ buf[i] = x ? buf[i] | m : buf[i] & ~m;
114
+ } else {
115
+ buf[i] = x ? buf[i] | m : buf[i] & ~m;
116
+ buf.fill(x ? -1 : 0, i + 1, j);
117
+ m = (1 << (end & 31)) - 1;
118
+ buf[j] = x ? buf[j] | m : buf[j] & ~m;
119
+ }
120
+ return this;
121
+ }
87
122
  ensure(size) {
88
123
  const b = this.buffer;
89
124
  if (!b || size >= b.length) {
@@ -1,15 +1,16 @@
1
+ import type { Predicate } from "@thi.ng/api";
1
2
  import type { BidirIndex } from "@thi.ng/bidir-index";
2
- import { type ColumnSpec, type IColumn, type SerializedColumn, type SerializedIndex } from "../api.js";
3
+ import { type ColumnID, type ColumnSpec, type IColumn, type Row, type SerializedColumn, type SerializedIndex } from "../api.js";
3
4
  import { BitmapIndex } from "../bitmap.js";
4
5
  import type { Table } from "../table.js";
5
- export declare abstract class AColumn implements IColumn {
6
- readonly id: string;
7
- table: Table;
6
+ export declare abstract class AColumn<T extends Row = Row> implements IColumn {
7
+ readonly id: ColumnID<T>;
8
+ table: Table<T>;
8
9
  spec: ColumnSpec;
9
10
  bitmap?: BitmapIndex;
10
11
  dict?: BidirIndex<any>;
11
12
  abstract isArray: boolean;
12
- constructor(id: string, table: Table);
13
+ constructor(id: ColumnID<T>, table: Table<T>);
13
14
  abstract load(spec: SerializedColumn): void;
14
15
  reindex(): void;
15
16
  abstract validate(value: any): boolean;
@@ -20,6 +21,9 @@ export declare abstract class AColumn implements IColumn {
20
21
  abstract getRow(i: number): any;
21
22
  abstract getRowKey(i: number): any;
22
23
  abstract indexOf(value: any, start?: number, end?: number): number;
24
+ abstract lastIndexOf(value: any, start?: number, end?: number): number;
25
+ findIndex(pred: Predicate<any>, start?: number, end?: number): number;
26
+ findLastIndex(pred: Predicate<any>, start?: number, end?: number): number;
23
27
  encode(value: any): any;
24
28
  decode(value: any): any;
25
29
  protected loadDict(serialized: SerializedIndex): void;
@@ -1,8 +1,9 @@
1
- import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
2
1
  import {
3
2
  FLAG_BITMAP
4
3
  } from "../api.js";
5
4
  import { BitmapIndex } from "../bitmap.js";
5
+ import { __columnError } from "../internal/checks.js";
6
+ import { __clampRange } from "../internal/indexof.js";
6
7
  class AColumn {
7
8
  constructor(id, table) {
8
9
  this.id = id;
@@ -16,6 +17,20 @@ class AColumn {
16
17
  reindex() {
17
18
  this.updateBitmap();
18
19
  }
20
+ findIndex(pred, start = 0, end) {
21
+ [start, end] = __clampRange(this.table.length, start, end);
22
+ for (let i = start; i < end; i++) {
23
+ if (pred(this.getRow(i))) return i;
24
+ }
25
+ return -1;
26
+ }
27
+ findLastIndex(pred, start = 0, end) {
28
+ [start, end] = __clampRange(this.table.length, start, end);
29
+ for (let i = end; i-- > start; ) {
30
+ if (pred(this.getRow(i))) return i;
31
+ }
32
+ return -1;
33
+ }
19
34
  encode(value) {
20
35
  return value;
21
36
  }
@@ -49,7 +64,7 @@ class AColumn {
49
64
  }
50
65
  }
51
66
  ensureValue(val) {
52
- return val != null ? val : this.spec.cardinality[0] > 0 ? this.spec.default ?? illegalArgs(`missing value for column: ${this.id}`) : this.spec.default ?? null;
67
+ return val != null ? val : this.spec.cardinality[0] > 0 ? this.spec.default ?? __columnError(this.id, `missing value`) : this.spec.default ?? null;
53
68
  }
54
69
  ensureBitmap() {
55
70
  if (this.spec.flags & FLAG_BITMAP) this.bitmap = new BitmapIndex();
@@ -1,7 +1,7 @@
1
1
  import { BidirIndex } from "@thi.ng/bidir-index";
2
- import { type IColumn, type SerializedColumn } from "../api.js";
2
+ import { type Row, type SerializedColumn } from "../api.js";
3
3
  import { AColumn } from "./acolumn.js";
4
- export declare class DictTupleColumn extends AColumn implements IColumn {
4
+ export declare class DictTupleColumn<T extends Row = Row> extends AColumn<T> {
5
5
  values: (number[] | null)[];
6
6
  dict: BidirIndex<any>;
7
7
  readonly isArray = true;
@@ -16,6 +16,7 @@ export declare class DictTupleColumn extends AColumn implements IColumn {
16
16
  valueKey(value: any): (number | null)[];
17
17
  removeRow(i: number): void;
18
18
  indexOf(value: any, start?: number, end?: number): number;
19
+ lastIndexOf(value: any, start?: number, end?: number): number;
19
20
  replaceValue(currValue: any, newValue: any): boolean;
20
21
  toJSON(): {
21
22
  dict: {
@@ -2,7 +2,7 @@ import { BidirIndex } from "@thi.ng/bidir-index";
2
2
  import { isArray } from "@thi.ng/checks/is-array";
3
3
  import { FLAG_UNIQUE } from "../api.js";
4
4
  import { __validateArrayValue } from "../internal/checks.js";
5
- import { __indexOfTuple } from "../internal/indexof.js";
5
+ import { __indexOfTuple, __lastIndexOfTuple } from "../internal/indexof.js";
6
6
  import { __serializeDict } from "../internal/serialize.js";
7
7
  import { AColumn } from "./acolumn.js";
8
8
  class DictTupleColumn extends AColumn {
@@ -65,6 +65,15 @@ class DictTupleColumn extends AColumn {
65
65
  end
66
66
  );
67
67
  }
68
+ lastIndexOf(value, start = 0, end) {
69
+ return __lastIndexOfTuple(
70
+ value != null ? this.encode(value) : null,
71
+ this.values,
72
+ this.table.length,
73
+ start,
74
+ end
75
+ );
76
+ }
68
77
  replaceValue(currValue, newValue) {
69
78
  const { dict, values, bitmap } = this;
70
79
  const res = dict.renameKey(currValue, newValue);
package/columns/dict.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { BidirIndex } from "@thi.ng/bidir-index";
2
- import { type IColumn, type SerializedColumn } from "../api.js";
2
+ import { type Row, type SerializedColumn } from "../api.js";
3
3
  import { AColumn } from "./acolumn.js";
4
- export declare class DictColumn extends AColumn implements IColumn {
4
+ export declare class DictColumn<T extends Row = Row> extends AColumn<T> {
5
5
  values: (number | null)[];
6
6
  dict: BidirIndex<any>;
7
7
  readonly isArray = false;
@@ -16,6 +16,7 @@ export declare class DictColumn extends AColumn implements IColumn {
16
16
  valueKey(value: any): number | number[] | undefined;
17
17
  removeRow(i: number): void;
18
18
  indexOf(value: any, start?: number, end?: number): number;
19
+ lastIndexOf(value: any, start?: number, end?: number): number;
19
20
  replaceValue(currValue: any, newValue: any): boolean;
20
21
  toJSON(): {
21
22
  dict: {
package/columns/dict.js CHANGED
@@ -4,7 +4,7 @@ import { decodeBinary, encodeBinary } from "@thi.ng/rle-pack/binary";
4
4
  import { decodeSimple, encodeSimple } from "@thi.ng/rle-pack/simple";
5
5
  import { FLAG_RLE } from "../api.js";
6
6
  import { __validateValue } from "../internal/checks.js";
7
- import { __indexOfSingle } from "../internal/indexof.js";
7
+ import { __indexOfSingle, __lastIndexOfSingle } from "../internal/indexof.js";
8
8
  import { __serializeDict } from "../internal/serialize.js";
9
9
  import { AColumn } from "./acolumn.js";
10
10
  class DictColumn extends AColumn {
@@ -68,6 +68,16 @@ class DictColumn extends AColumn {
68
68
  end
69
69
  );
70
70
  }
71
+ lastIndexOf(value, start = 0, end) {
72
+ return __lastIndexOfSingle(
73
+ this.encode(value),
74
+ this.values,
75
+ this.bitmap,
76
+ this.table.length,
77
+ start,
78
+ end
79
+ );
80
+ }
71
81
  replaceValue(currValue, newValue) {
72
82
  const { dict, values, bitmap } = this;
73
83
  const res = dict.renameKey(currValue, newValue);
@@ -1,6 +1,6 @@
1
- import { type IColumn, type SerializedColumn } from "../api.js";
1
+ import { type Row, type SerializedColumn } from "../api.js";
2
2
  import { AColumn } from "./acolumn.js";
3
- export declare class PlainColumn extends AColumn implements IColumn {
3
+ export declare class PlainColumn<T extends Row = Row> extends AColumn<T> {
4
4
  values: any[];
5
5
  readonly isArray = false;
6
6
  load({ values }: SerializedColumn): void;
@@ -11,6 +11,7 @@ export declare class PlainColumn extends AColumn implements IColumn {
11
11
  valueKey(x: any): any;
12
12
  removeRow(i: number): void;
13
13
  indexOf(value: any, start?: number, end?: number): number;
14
+ lastIndexOf(value: any, start?: number, end?: number): number;
14
15
  replaceValue(currValue: any, newValue: any): boolean;
15
16
  toJSON(): {
16
17
  values: any[];
package/columns/plain.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { decodeSimple, encodeSimple } from "@thi.ng/rle-pack/simple";
2
2
  import { FLAG_RLE } from "../api.js";
3
3
  import { __validateValue } from "../internal/checks.js";
4
- import { __indexOfSingle } from "../internal/indexof.js";
4
+ import { __indexOfSingle, __lastIndexOfSingle } from "../internal/indexof.js";
5
5
  import { __replaceValue } from "../internal/replace.js";
6
6
  import { AColumn } from "./acolumn.js";
7
7
  class PlainColumn extends AColumn {
@@ -46,6 +46,16 @@ class PlainColumn extends AColumn {
46
46
  end
47
47
  );
48
48
  }
49
+ lastIndexOf(value, start = 0, end) {
50
+ return __lastIndexOfSingle(
51
+ value,
52
+ this.values,
53
+ this.bitmap,
54
+ this.table.length,
55
+ start,
56
+ end
57
+ );
58
+ }
49
59
  replaceValue(currValue, newValue) {
50
60
  return __replaceValue(this.bitmap, this.values, currValue, newValue);
51
61
  }
@@ -1,7 +1,7 @@
1
1
  import type { Nullable } from "@thi.ng/api";
2
- import { type IColumn, type SerializedColumn } from "../api.js";
2
+ import { type Row, type SerializedColumn } from "../api.js";
3
3
  import { AColumn } from "./acolumn.js";
4
- export declare class TupleColumn extends AColumn implements IColumn {
4
+ export declare class TupleColumn<T extends Row = Row> extends AColumn<T> {
5
5
  values: Nullable<number[]>[];
6
6
  readonly isArray = true;
7
7
  load(spec: SerializedColumn): void;
@@ -13,6 +13,7 @@ export declare class TupleColumn extends AColumn implements IColumn {
13
13
  valueKey(value: any): any[];
14
14
  removeRow(i: number): void;
15
15
  indexOf(value: any, start?: number, end?: number): number;
16
+ lastIndexOf(value: any, start?: number, end?: number): number;
16
17
  replaceValue(currValue: any, newValue: any): boolean;
17
18
  toJSON(): {
18
19
  values: Nullable<number[]>[];
package/columns/tuple.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { isArray } from "@thi.ng/checks/is-array";
2
2
  import { FLAG_UNIQUE } from "../api.js";
3
3
  import { __validateArrayValue } from "../internal/checks.js";
4
- import { __indexOfTuple } from "../internal/indexof.js";
4
+ import { __indexOfTuple, __lastIndexOfTuple } from "../internal/indexof.js";
5
5
  import { AColumn } from "./acolumn.js";
6
6
  class TupleColumn extends AColumn {
7
7
  values = [];
@@ -48,6 +48,15 @@ class TupleColumn extends AColumn {
48
48
  end
49
49
  );
50
50
  }
51
+ lastIndexOf(value, start = 0, end) {
52
+ return __lastIndexOfTuple(
53
+ value,
54
+ this.values,
55
+ this.table.length,
56
+ start,
57
+ end
58
+ );
59
+ }
51
60
  replaceValue(currValue, newValue) {
52
61
  const { values, bitmap } = this;
53
62
  const isUnique = this.spec.flags & FLAG_UNIQUE;
@@ -1,14 +1,14 @@
1
1
  import { type TypedArray } from "@thi.ng/api/typedarray";
2
- import { type IColumn, type NumericType, type SerializedColumn } from "../api.js";
2
+ import { type ColumnID, type NumericType, type Row, type SerializedColumn } from "../api.js";
3
3
  import type { Table } from "../table.js";
4
4
  import { AColumn } from "./acolumn.js";
5
- export declare class TypedArrayColumn extends AColumn implements IColumn {
5
+ export declare class TypedArrayColumn<T extends Row = Row> extends AColumn<T> {
6
6
  values: TypedArray;
7
7
  type: NumericType;
8
8
  limit: [number, number];
9
9
  protected tmp: TypedArray;
10
10
  readonly isArray = false;
11
- constructor(id: string, table: Table);
11
+ constructor(id: ColumnID<T>, table: Table<T>);
12
12
  load({ values }: SerializedColumn): void;
13
13
  validate(value: any): boolean;
14
14
  setRow(i: number, value: any): void;
@@ -17,6 +17,7 @@ export declare class TypedArrayColumn extends AColumn implements IColumn {
17
17
  valueKey(value: any): number | number[];
18
18
  removeRow(i: number): void;
19
19
  indexOf(value: any, start?: number, end?: number): number;
20
+ lastIndexOf(value: any, start?: number, end?: number): number;
20
21
  replaceValue(currValue: any, newValue: any): boolean;
21
22
  toJSON(): {
22
23
  values: any[];
@@ -4,7 +4,7 @@ import { isNumber } from "@thi.ng/checks/is-number";
4
4
  import {
5
5
  LIMITS
6
6
  } from "../api.js";
7
- import { __indexOfSingle } from "../internal/indexof.js";
7
+ import { __indexOfSingle, __lastIndexOfSingle } from "../internal/indexof.js";
8
8
  import { __replaceValue } from "../internal/replace.js";
9
9
  import { __deserializeTyped, __serializeTyped } from "../internal/serialize.js";
10
10
  import { AColumn } from "./acolumn.js";
@@ -78,6 +78,16 @@ class TypedArrayColumn extends AColumn {
78
78
  end
79
79
  );
80
80
  }
81
+ lastIndexOf(value, start = 0, end) {
82
+ return __lastIndexOfSingle(
83
+ value,
84
+ this.values,
85
+ this.bitmap,
86
+ this.table.length,
87
+ start,
88
+ end
89
+ );
90
+ }
81
91
  replaceValue(currValue, newValue) {
82
92
  return __replaceValue(this.bitmap, this.values, currValue, newValue);
83
93
  }
@@ -1,15 +1,15 @@
1
1
  import { type TypedArray } from "@thi.ng/api/typedarray";
2
- import { type IColumn, type NumericType, type SerializedColumn } from "../api.js";
2
+ import { type ColumnID, type NumericType, type Row, type SerializedColumn } from "../api.js";
3
3
  import type { Table } from "../table.js";
4
4
  import { AColumn } from "./acolumn.js";
5
- export declare class VectorColumn extends AColumn implements IColumn {
5
+ export declare class VectorColumn<T extends Row = Row> extends AColumn<T> {
6
6
  values: TypedArray;
7
7
  type: NumericType;
8
8
  size: number;
9
9
  limit: [number, number];
10
10
  protected tmp: TypedArray;
11
11
  readonly isArray = false;
12
- constructor(id: string, table: Table);
12
+ constructor(id: ColumnID<T>, table: Table<T>);
13
13
  load({ values }: SerializedColumn): void;
14
14
  validate(value: any): boolean;
15
15
  setRow(i: number, value: any): void;
@@ -18,6 +18,7 @@ export declare class VectorColumn extends AColumn implements IColumn {
18
18
  valueKey(value: any): string | string[];
19
19
  removeRow(i: number): void;
20
20
  indexOf(value: any, start?: number, end?: number): number;
21
+ lastIndexOf(value: any, start?: number, end?: number): number;
21
22
  replaceValue(): boolean;
22
23
  toJSON(): {
23
24
  values: any[];
package/columns/vector.js CHANGED
@@ -6,6 +6,7 @@ import { unsupportedOp } from "@thi.ng/errors/unsupported";
6
6
  import {
7
7
  LIMITS
8
8
  } from "../api.js";
9
+ import { __clampRange } from "../internal/indexof.js";
9
10
  import { __deserializeTyped, __serializeTyped } from "../internal/serialize.js";
10
11
  import { AColumn } from "./acolumn.js";
11
12
  class VectorColumn extends AColumn {
@@ -76,10 +77,10 @@ class VectorColumn extends AColumn {
76
77
  this.values.fill(0, (length - 1) * size);
77
78
  this.bitmap?.removeBit(i);
78
79
  }
79
- indexOf(value, start = 0, end = this.table.length) {
80
+ indexOf(value, start = 0, end) {
81
+ if (value == null) return -1;
80
82
  const { values, bitmap, size } = this;
81
- start = Math.max(start, 0);
82
- end = Math.min(end, this.table.length);
83
+ [start, end] = __clampRange(this.table.length, start, end);
83
84
  if (bitmap) {
84
85
  return bitmap.index.get(this.valueKey(value))?.first(start, end) ?? -1;
85
86
  }
@@ -93,6 +94,23 @@ class VectorColumn extends AColumn {
93
94
  }
94
95
  return -1;
95
96
  }
97
+ lastIndexOf(value, start = 0, end) {
98
+ if (value == null || value.length !== this.size) return -1;
99
+ const { values, bitmap, size } = this;
100
+ [start, end] = __clampRange(this.table.length, start, end);
101
+ if (bitmap) {
102
+ return bitmap.index.get(this.valueKey(value))?.last(start, end) ?? -1;
103
+ }
104
+ start *= size;
105
+ let i, j;
106
+ outer: for (i = end * size; (i -= size) >= start; ) {
107
+ for (j = 0; j < size; j++) {
108
+ if (values[i + j] !== value[j]) continue outer;
109
+ }
110
+ return i / size;
111
+ }
112
+ return -1;
113
+ }
96
114
  replaceValue() {
97
115
  unsupportedOp("TODO");
98
116
  }
@@ -2,5 +2,14 @@ import type { Maybe } from "@thi.ng/api";
2
2
  import type { BitmapIndex } from "../bitmap.js";
3
3
  /** @internal */
4
4
  export declare const __indexOfSingle: (needle: any, values: ArrayLike<any>, bitmap: Maybe<BitmapIndex>, max: number, start: number, end?: number) => number;
5
+ /** @internal */
6
+ export declare const __lastIndexOfSingle: (needle: any, values: ArrayLike<any>, bitmap: Maybe<BitmapIndex>, max: number, start: number, end?: number) => number;
7
+ /** @internal */
5
8
  export declare const __indexOfTuple: (needle: ArrayLike<any> | null, values: ArrayLike<any>, max: number, start: number, end?: number) => number;
9
+ /** @internal */
10
+ export declare const __lastIndexOfTuple: (needle: ArrayLike<any> | null, values: ArrayLike<any>, max: number, start: number, end?: number) => number;
11
+ /** @internal */
12
+ export declare const __clamp: (x: number, a: number, b: number) => number;
13
+ /** @internal */
14
+ export declare const __clampRange: (max: number, start: number, end?: number) => number[];
6
15
  //# sourceMappingURL=indexof.d.ts.map
@@ -1,19 +1,26 @@
1
1
  const __indexOfSingle = (needle, values, bitmap, max, start, end = max) => {
2
- start = Math.max(start, 0);
3
- end = Math.min(end, max);
2
+ [start, end] = __clampRange(max, start, end);
4
3
  if (bitmap) return bitmap.index.get(needle)?.first(start, end) ?? -1;
5
4
  for (let i = start; i < end; i++) {
6
5
  if (values[i] === needle) return i;
7
6
  }
8
7
  return -1;
9
8
  };
9
+ const __lastIndexOfSingle = (needle, values, bitmap, max, start, end = max) => {
10
+ [start, end] = __clampRange(max, start, end);
11
+ if (bitmap) return bitmap.index.get(needle)?.last(start, end) ?? -1;
12
+ for (let i = end; i-- > start; ) {
13
+ if (values[i] === needle) return i;
14
+ }
15
+ return -1;
16
+ };
10
17
  const __indexOfTuple = (needle, values, max, start, end = max) => {
11
- start = Math.max(start, 0);
12
- end = Math.min(end, max);
18
+ [start, end] = __clampRange(max, start, end);
13
19
  if (needle == null) {
14
20
  for (let i2 = start; i2 < end; i2++) {
15
21
  if (!values[i2]) return i2;
16
22
  }
23
+ return -1;
17
24
  }
18
25
  const n = needle.length;
19
26
  let i, j, row;
@@ -28,7 +35,38 @@ const __indexOfTuple = (needle, values, max, start, end = max) => {
28
35
  }
29
36
  return -1;
30
37
  };
38
+ const __lastIndexOfTuple = (needle, values, max, start, end = max) => {
39
+ [start, end] = __clampRange(max, start, end);
40
+ if (needle == null) {
41
+ for (let i2 = end; i2-- > start; ) {
42
+ if (!values[i2]) return i2;
43
+ }
44
+ return -1;
45
+ }
46
+ const n = needle.length;
47
+ let i, j, row;
48
+ outer: for (i = end; i-- > start; ) {
49
+ row = values[i];
50
+ if (row?.length === n) {
51
+ for (j = 0; j < n; j++) {
52
+ if (row[j] !== needle[j]) continue outer;
53
+ }
54
+ return i;
55
+ }
56
+ }
57
+ return -1;
58
+ };
59
+ const __clamp = (x, a, b) => x < a ? a : x > b ? b : x;
60
+ const __clampRange = (max, start, end) => {
61
+ start = __clamp(start, 0, max);
62
+ end = __clamp(end ?? max, start, max);
63
+ return [start, end];
64
+ };
31
65
  export {
66
+ __clamp,
67
+ __clampRange,
32
68
  __indexOfSingle,
33
- __indexOfTuple
69
+ __indexOfTuple,
70
+ __lastIndexOfSingle,
71
+ __lastIndexOfTuple
34
72
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thi.ng/column-store",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
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",
@@ -126,5 +126,5 @@
126
126
  "status": "alpha",
127
127
  "year": 2025
128
128
  },
129
- "gitHead": "3f0b151fb2283888314eb34206ba50c2dd5c7af6\n"
129
+ "gitHead": "f1d8da5644dd22f212fc313df2187380da968a7c\n"
130
130
  }
package/query.d.ts CHANGED
@@ -1,30 +1,33 @@
1
1
  import type { Predicate } from "@thi.ng/api";
2
- import type { QueryTerm, QueryTermOpSpec } from "./api.js";
2
+ import type { ColumnID, QueryTerm, QueryTermOpSpec, Row } from "./api.js";
3
3
  import type { Table } from "./table.js";
4
- export declare class Query {
5
- table: Table;
6
- terms: QueryTerm[];
7
- constructor(table: Table, terms?: QueryTerm[]);
8
- addTerm(term: QueryTerm): this;
4
+ export declare class Query<T extends Row> {
5
+ readonly table: Table<T>;
6
+ terms: QueryTerm<T>[];
7
+ constructor(table: Table<T>, terms?: QueryTerm<T>[]);
8
+ addTerm(term: QueryTerm<T>): this;
9
9
  /** Alias for {@link Query.or} */
10
- where: (column: string, value: any) => this;
10
+ where: (column: ColumnID<T>, value: any) => this;
11
11
  /** Alias for {@link Query.nor} */
12
- whereNot: (column: string, value: any) => this;
13
- or(column: string, value: any): this;
14
- nor(column: string, value: any): this;
15
- and(column: string, value: any): this;
16
- nand(column: string, value: any): this;
17
- matchColumn(column: string, pred: Predicate<any>): this;
18
- matchPartialRow(columns: string[], pred: Predicate<Record<string, any>>): this;
19
- matchRow(pred: Predicate<Record<string, any>>): this;
20
- [Symbol.iterator](): Generator<import("./api.js").Row | undefined, void, unknown>;
12
+ whereNot: (column: ColumnID<T>, value: any) => this;
13
+ or(column: ColumnID<T>, value: any): this;
14
+ nor(column: ColumnID<T>, value: any): this;
15
+ and(column: ColumnID<T>, value: any): this;
16
+ nand(column: ColumnID<T>, value: any): this;
17
+ matchColumn(column: ColumnID<T>, pred: Predicate<any>): this;
18
+ matchPartialRow<K extends ColumnID<T>>(columns: K[], pred: Predicate<Pick<T, K>>): this;
19
+ matchRow(pred: Predicate<T>): this;
20
+ valueRange(column: ColumnID<T>, start: any, end?: any): this;
21
+ rowRange(start?: number, end?: number): this;
22
+ [Symbol.iterator](): Generator<import("./api.js").RowWithMeta<T>, void, unknown>;
21
23
  }
22
- export declare class QueryCtx {
23
- readonly query: Query;
24
- readonly table: Table;
24
+ export declare class QueryCtx<T extends Row> {
25
+ readonly query: Query<T>;
26
+ readonly table: Table<T>;
25
27
  readonly size: number;
26
28
  bitmap?: Uint32Array;
27
- constructor(query: Query);
29
+ constructor(query: Query<T>);
30
+ clear(): void;
28
31
  makeMask(seed?: Uint32Array): Uint32Array<ArrayBuffer>;
29
32
  /**
30
33
  * Combines the `mask` with the context's mask (combined using bitwise AND).
package/query.js CHANGED
@@ -2,6 +2,8 @@ import { isArray } from "@thi.ng/checks/is-array";
2
2
  import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
3
3
  import { unsupportedOp } from "@thi.ng/errors/unsupported";
4
4
  import { Bitfield } from "./bitmap.js";
5
+ import { __columnError } from "./internal/checks.js";
6
+ import { __clampRange } from "./internal/indexof.js";
5
7
  class Query {
6
8
  constructor(table, terms = []) {
7
9
  this.table = table;
@@ -49,6 +51,19 @@ class Query {
49
51
  this.terms.push({ type: "matchRow", value: pred });
50
52
  return this;
51
53
  }
54
+ valueRange(column, start, end) {
55
+ if (this.table.columns[column].isArray)
56
+ __columnError(
57
+ column,
58
+ `operator not supported with this column type`
59
+ );
60
+ this.terms.push({ type: "valueRange", column, value: { start, end } });
61
+ return this;
62
+ }
63
+ rowRange(start = 0, end) {
64
+ this.terms.push({ type: "rowRange", value: { start, end } });
65
+ return this;
66
+ }
52
67
  *[Symbol.iterator]() {
53
68
  const { table } = this;
54
69
  const ctx = new QueryCtx(this);
@@ -57,7 +72,7 @@ class Query {
57
72
  let column;
58
73
  if (term.column) {
59
74
  column = ctx.table.columns[term.column];
60
- if (!column) illegalArgs(`column: ${term.column}`);
75
+ if (!column) illegalArgs(`column: ${String(term.column)}`);
61
76
  } else if (QUERY_OPS[term.type].mode !== "row") {
62
77
  illegalArgs(
63
78
  `query op: ${term.type} requires a column name given`
@@ -80,6 +95,9 @@ class QueryCtx {
80
95
  table;
81
96
  size;
82
97
  bitmap;
98
+ clear() {
99
+ this.bitmap = void 0;
100
+ }
83
101
  makeMask(seed) {
84
102
  const mask = new Uint32Array(this.size);
85
103
  if (seed) mask.set(seed);
@@ -174,7 +192,7 @@ const execBitAnd = (ctx, term, column) => {
174
192
  for (let k of key) {
175
193
  const b = bitmap.index.get(k)?.buffer;
176
194
  if (!b) {
177
- if (term.type === "and") ctx.bitmap = void 0;
195
+ if (term.type === "and") ctx.clear();
178
196
  return;
179
197
  }
180
198
  colBitmaps.push(b);
@@ -191,9 +209,7 @@ const execBitAnd = (ctx, term, column) => {
191
209
  }
192
210
  if (mask) {
193
211
  term.type === "nand" ? ctx.mergeInvMask(mask) : ctx.mergeMask(mask);
194
- } else {
195
- ctx.bitmap = void 0;
196
- }
212
+ } else ctx.clear();
197
213
  };
198
214
  const execAnd = (ctx, term, column) => {
199
215
  const n = ctx.table.length;
@@ -213,7 +229,7 @@ const execAnd = (ctx, term, column) => {
213
229
  for (let i = 0; i < n; i++) mask[i] &= m[i];
214
230
  } else mask = m;
215
231
  } else {
216
- if (term.type === "and") ctx.bitmap = void 0;
232
+ if (term.type === "and") ctx.clear();
217
233
  return;
218
234
  }
219
235
  }
@@ -253,7 +269,7 @@ const QUERY_OPS = {
253
269
  const columns = term.params;
254
270
  const pred = term.value;
255
271
  let mask;
256
- for (let i = 0, n = ctx.table.length; i < n; i++) {
272
+ for (let i = 0, n = table.length; i < n; i++) {
257
273
  if (pred(table.getPartialRow(i, columns, false))) {
258
274
  if (!mask) mask = ctx.makeMask();
259
275
  mask[i >>> 5] |= 1 << (i & 31);
@@ -268,7 +284,7 @@ const QUERY_OPS = {
268
284
  const table = ctx.table;
269
285
  const pred = term.value;
270
286
  let mask;
271
- for (let i = 0, n = ctx.table.length; i < n; i++) {
287
+ for (let i = 0, n = table.length; i < n; i++) {
272
288
  if (pred(table.getRow(i, false))) {
273
289
  if (!mask) mask = ctx.makeMask();
274
290
  mask[i >>> 5] |= 1 << (i & 31);
@@ -276,8 +292,37 @@ const QUERY_OPS = {
276
292
  }
277
293
  if (mask) ctx.mergeMask(mask);
278
294
  }
295
+ },
296
+ valueRange: {
297
+ fn: (ctx, term, column) => {
298
+ const max = ctx.table.length;
299
+ const { start, end } = term.value;
300
+ let $start = 0;
301
+ let $end = max;
302
+ if (start != null) $start = column.findIndex((x) => x >= start);
303
+ if (end != null)
304
+ $end = column.findLastIndex((x) => x <= end, $start);
305
+ __fillMask(ctx, $start, $end >= 0 ? $end + 1 : $end);
306
+ }
307
+ },
308
+ rowRange: {
309
+ mode: "row",
310
+ fn: (ctx, term) => {
311
+ const max = ctx.table.length;
312
+ let { start = 0, end = max } = term.value;
313
+ [start, end] = __clampRange(max, start, end);
314
+ __fillMask(ctx, start, end);
315
+ }
279
316
  }
280
317
  };
318
+ const __fillMask = (ctx, start, end, fill = 1) => {
319
+ if (start >= 0 && end >= 0) {
320
+ const mask = ctx.makeMask();
321
+ const bitmap = new Bitfield(mask);
322
+ bitmap.fill(fill, start, end);
323
+ ctx.mergeMask(mask);
324
+ } else ctx.clear();
325
+ };
281
326
  const registerQueryOp = (type, spec) => {
282
327
  if (QUERY_OPS[type]) illegalArgs(`query op ${type} already registered`);
283
328
  QUERY_OPS[type] = spec;
package/table.d.ts CHANGED
@@ -1,38 +1,44 @@
1
- import { type ColumnSchema, type ColumnSpec, type ColumnTypeSpec, type IColumn, type QueryTerm, type Row, type SerializedTable } from "./api.js";
1
+ import type { Maybe } from "@thi.ng/api";
2
+ import { type ColumnID, type ColumnSchema, type ColumnSpec, type ColumnTypeSpec, type IColumn, type QueryTerm, type Row, type RowWithMeta, type SerializedTable } from "./api.js";
2
3
  import { Query } from "./query.js";
3
4
  /**
4
5
  * Placeholder only. Unused so far.
5
6
  */
6
7
  export interface TableOpts {
7
8
  }
8
- export declare class Table {
9
+ export declare class Table<T extends Row> {
9
10
  opts: TableOpts;
10
- schema: ColumnSchema;
11
- columns: Record<string, IColumn>;
11
+ schema: ColumnSchema<T>;
12
+ columns: Record<ColumnID<T>, IColumn>;
12
13
  length: number;
13
- static load(serialized: SerializedTable, opts?: Partial<TableOpts>): Table;
14
- constructor(schema: Record<string, Partial<ColumnSpec> & {
14
+ static load<T extends Row>(serialized: SerializedTable<T>, opts?: Partial<TableOpts>): Table<T>;
15
+ constructor(schema: Record<ColumnID<T>, Partial<ColumnSpec> & {
15
16
  type: ColumnSpec["type"];
16
17
  }>, opts?: Partial<TableOpts>);
17
- query(terms?: QueryTerm[]): Query;
18
- addColumn(id: string, spec: Partial<ColumnSpec> & {
18
+ query(terms?: QueryTerm<T>[]): Query<T>;
19
+ addColumn(id: ColumnID<T>, spec: Partial<ColumnSpec> & {
19
20
  type: ColumnSpec["type"];
20
21
  }): void;
21
- removeColumn(id: string): boolean;
22
- [Symbol.iterator](): Generator<Row | undefined, void, unknown>;
22
+ removeColumn(id: ColumnID<T>): boolean;
23
+ [Symbol.iterator](): Generator<Maybe<T>, void, unknown>;
23
24
  reindex(): void;
24
- addRow(row: Row): void;
25
- addRows(rows: Iterable<Row>): void;
26
- updateRow(i: number, row: Row): void;
25
+ addRow(row: Partial<T>): void;
26
+ addRows(rows: Iterable<Partial<T>>): void;
27
+ updateRow(i: number, row: T): void;
27
28
  removeRow(i: number): void;
28
- getRow(i: number, safe?: boolean, includeID?: boolean): Row | undefined;
29
- getPartialRow(i: number, columns: string[], safe?: boolean, includeID?: boolean): Row | undefined;
30
- indexOf(id: string, value: any, start?: number, end?: number): number;
31
- validateRow(row: Row): void;
32
- validateColumnSpec(id: string, spec: ColumnSpec): void;
29
+ getRow(i: number, safe?: boolean): Maybe<T>;
30
+ getRow(i: number, safe?: boolean, includeID?: false): Maybe<T>;
31
+ getRow(i: number, safe?: boolean, includeID?: true): Maybe<RowWithMeta<T>>;
32
+ getPartialRow<K extends ColumnID<T>>(i: number, columns: K[], safe?: boolean): Maybe<Pick<T, K>>;
33
+ getPartialRow<K extends ColumnID<T>>(i: number, columns: K[], safe?: boolean, includeID?: false): Maybe<Pick<T, K>>;
34
+ getPartialRow<K extends ColumnID<T>>(i: number, columns: K[], safe?: boolean, includeID?: true): Maybe<RowWithMeta<Pick<T, K>>>;
35
+ indexOf(id: ColumnID<T>, value: any, start?: number, end?: number): number;
36
+ lastIndexOf(id: ColumnID<T>, value: any, start?: number, end?: number): number;
37
+ validateRow(row: Partial<T>): void;
38
+ validateColumnSpec(id: ColumnID<T>, spec: ColumnSpec): void;
33
39
  toJSON(): {
34
- schema: ColumnSchema;
35
- columns: Record<string, IColumn>;
40
+ schema: ColumnSchema<T>;
41
+ columns: Record<ColumnID<T>, IColumn>;
36
42
  length: number;
37
43
  };
38
44
  }
package/table.js CHANGED
@@ -43,7 +43,11 @@ class Table {
43
43
  };
44
44
  this.validateColumnSpec(id, $spec);
45
45
  this.schema[id] = $spec;
46
- this.columns[id] = COLUMN_TYPES[spec.type].impl(this, id, $spec);
46
+ this.columns[id] = COLUMN_TYPES[spec.type].impl(
47
+ this,
48
+ String(id),
49
+ $spec
50
+ );
47
51
  }
48
52
  removeColumn(id) {
49
53
  if (this.columns[id]) return false;
@@ -101,6 +105,9 @@ class Table {
101
105
  indexOf(id, value, start, end) {
102
106
  return this.columns[id]?.indexOf(value, start, end) ?? -1;
103
107
  }
108
+ lastIndexOf(id, value, start, end) {
109
+ return this.columns[id]?.lastIndexOf(value, start, end) ?? -1;
110
+ }
104
111
  validateRow(row) {
105
112
  const { columns } = this;
106
113
  for (let id in columns) {