@infino-ai/infino 0.9.0 → 0.9.2

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/infino/index.d.ts CHANGED
@@ -19,6 +19,60 @@ export type SchemaDescriptor = Record<string, string | {
19
19
  }>;
20
20
  /** Accepted shapes for `Table.append`. */
21
21
  export type AppendData = RowRecord[] | arrow.Table | arrow.RecordBatch | Buffer | Uint8Array;
22
+ /** A column's index in the schema document: full-text or vector. */
23
+ export type ColumnIndex = {
24
+ kind: "fts";
25
+ analyzer: string;
26
+ stopwords?: string;
27
+ stemmer?: string;
28
+ positions: boolean;
29
+ stored: boolean;
30
+ k1: number;
31
+ b: number;
32
+ } | {
33
+ kind: "vector";
34
+ metric: Metric;
35
+ rot_seed: string;
36
+ rerank_codec: string;
37
+ };
38
+ /** One field of the schema document. `type` is the document's type vocabulary (`"i64"`, `"large_utf8"`, …); a vector column carries `dim`. */
39
+ export interface SchemaField {
40
+ id: number;
41
+ name: string;
42
+ type: string;
43
+ dim?: number;
44
+ nullable: boolean;
45
+ index?: ColumnIndex;
46
+ converting_from?: {
47
+ type: string;
48
+ dim?: number;
49
+ };
50
+ }
51
+ /** The schema document `Connection.schema` returns. */
52
+ export interface TableSchema {
53
+ schema_id: number;
54
+ last_field_id: number;
55
+ max_fields: number;
56
+ max_depth: number;
57
+ fields: SchemaField[];
58
+ tombstoned: number[];
59
+ }
60
+ /** One field of a schema patch: a new column (`type` required), or a change to a live one addressed by `id` or `name`. */
61
+ export interface SchemaFieldPatch {
62
+ id?: number;
63
+ name: string;
64
+ type?: string;
65
+ dim?: number;
66
+ nullable?: boolean;
67
+ index?: ColumnIndex;
68
+ dropped?: boolean;
69
+ }
70
+ /** What `Connection.schema(name, patch)` accepts: the document's own shape, every key optional. */
71
+ export interface SchemaPatch {
72
+ fields?: SchemaFieldPatch[];
73
+ max_fields?: number;
74
+ max_depth?: number;
75
+ }
22
76
  /**
23
77
  * Storage and cache config the `connect` URI can't carry. All optional.
24
78
  *
@@ -138,6 +192,9 @@ export interface ReindexReport {
138
192
  /** Columns whose text was never stored, so no repair can fix their terms;
139
193
  * only re-ingesting them from source can. */
140
194
  unrepairableColumns: string[];
195
+ /** Superfiles left untouched because their footer places a blob where
196
+ * the file or its manifest entry contradicts; they need inspection. */
197
+ inconsistentFooters: string[];
141
198
  }
142
199
  /** What a `reindex` would do, and what it would cost. */
143
200
  export interface StalenessReport {
@@ -152,6 +209,9 @@ export interface StalenessReport {
152
209
  bytesToRewrite: number;
153
210
  /** Columns no repair can fix, because their text was never stored. */
154
211
  unrepairableColumns: string[];
212
+ /** Superfiles whose footer places a blob where the file or its manifest
213
+ * entry contradicts. A reindex reports these and never rewrites them. */
214
+ inconsistentFooters: string[];
155
215
  /** Whether a reindex would do nothing at all. */
156
216
  isCurrent: boolean;
157
217
  }
@@ -334,6 +394,17 @@ export declare class Connection {
334
394
  /** Create a table from an apache-arrow `Schema` or `{ column: type }`. */
335
395
  createTable(name: string, schema: arrow.Schema | SchemaDescriptor | Buffer, indexes: IndexSpec): Table;
336
396
  openTable(name: string): Table;
397
+ /**
398
+ * The schema document of `name`: its fields with ids, types, nullability
399
+ * and indexes, the field cap and the `schema_id`. With `patch` (the same
400
+ * shape), merge it into the schema — or create the table from it when
401
+ * there is none — and return the document afterwards. A field is matched
402
+ * by `id` when it carries one and by `name` otherwise; an unmatched field
403
+ * is added, a different type changes the column, `dropped: true` retires
404
+ * it, and a field not mentioned is untouched. `expectedSchemaId` is a
405
+ * compare-and-set against the current `schema_id`.
406
+ */
407
+ schema(name: string, patch?: SchemaPatch, expectedSchemaId?: number): TableSchema;
337
408
  /** Drop a table. `purge` defaults to `true`, which also deletes the table's storage; pass `false` to only unregister it and keep the bytes. */
338
409
  dropTable(name: string, purge?: boolean): void;
339
410
  listTables(): string[];
package/infino/index.js CHANGED
@@ -134,33 +134,58 @@ function buildColumn(field, rows) {
134
134
  }
135
135
  return arrow.vectorFromArray(values, field.type);
136
136
  }
137
- // Normalize append input -> IPC bytes. An array of objects, or an
138
- // apache-arrow Table / RecordBatch (normalized to rows via its own
139
- // `toArray()`/`toJSON()`); either way the columns are rebuilt in our arrow
140
- // instance from the declared schema. (We can't feed the consumer's Table
141
- // straight into our `tableToIPC` — a different module instance isn't
142
- // recognized.)
143
- function dataToIpc(data, getSchema) {
137
+ // A `bigint` has no JSON form, so it travels as a one-key object the
138
+ // replacer alone can produce and is then spliced back to the integer
139
+ // literal it is — exact, and an integer rather than a float or a string.
140
+ // A string in the data cannot be mistaken for one: `JSON.stringify`
141
+ // escapes the quotes of a user's string, and the pattern below matches
142
+ // only unescaped ones.
143
+ const BIGINT_KEY = "$infino$bigint";
144
+ function rowsToJson(rows) {
145
+ const text = JSON.stringify(rows, (_key, value) => typeof value === "bigint" ? { [BIGINT_KEY]: value.toString() } : value);
146
+ return text.replace(/\{"\$infino\$bigint":"(-?\d+)"\}/g, "$1");
147
+ }
148
+ // Values JSON has no faithful spelling for. A `Buffer` stringifies to
149
+ // `{"type":"Buffer","data":[...]}` and a `Date` to a string, so neither
150
+ // throws on the document path and neither arrives as what it was. Rows
151
+ // carrying them are refused with the route that does carry them, rather
152
+ // than written as the object or the string JSON turned them into.
153
+ function unspellableValue(rows) {
154
+ for (const row of rows) {
155
+ for (const value of Object.values(row)) {
156
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(value))
157
+ return "a Buffer";
158
+ if (value instanceof Uint8Array)
159
+ return "a Uint8Array";
160
+ if (value instanceof Date)
161
+ return "a Date";
162
+ }
163
+ }
164
+ return undefined;
165
+ }
166
+ function writeInput(data) {
144
167
  if (Buffer.isBuffer(data))
145
- return data;
168
+ return { ipc: data };
146
169
  if (data instanceof Uint8Array)
147
- return Buffer.from(data);
148
- let rows;
149
- const d = data;
170
+ return { ipc: Buffer.from(data) };
150
171
  if (Array.isArray(data)) {
151
- rows = data;
152
- }
153
- else if (d && (Array.isArray(d.batches) || (d.schema && typeof d.numRows === "number"))) {
154
- rows = Array.from(d).map((r) => r.toJSON());
155
- }
156
- else {
157
- throw new TypeError("append: expected an array of objects, an apache-arrow Table / RecordBatch, or an Arrow IPC Buffer");
172
+ const unspellable = unspellableValue(data);
173
+ if (unspellable) {
174
+ throw new TypeError(`append: ${unspellable} has no document form and would be written as what JSON ` +
175
+ "turned it into; pass an apache-arrow Table or RecordBatch to keep its type");
176
+ }
177
+ return { rows: rowsToJson(data) };
158
178
  }
159
- const schema = getSchema();
160
- const cols = {};
161
- for (const field of schema.fields)
162
- cols[field.name] = buildColumn(field, rows);
163
- return Buffer.from(arrow.tableToIPC(new arrow.Table(cols), STREAM));
179
+ const d = data;
180
+ if (d && (Array.isArray(d.batches) || (d.schema && typeof d.numRows === "number"))) {
181
+ const rows = Array.from(d).map((r) => r.toJSON());
182
+ const fields = d.schema.fields.map((f) => new arrow.Field(f.name, nativeTypeFromForeign(f.type), f.nullable));
183
+ const cols = {};
184
+ for (const field of fields)
185
+ cols[field.name] = buildColumn(field, rows);
186
+ return { ipc: Buffer.from(arrow.tableToIPC(new arrow.Table(cols), STREAM)) };
187
+ }
188
+ throw new TypeError("append: expected an array of objects, an apache-arrow Table / RecordBatch, or an Arrow IPC Buffer");
164
189
  }
165
190
  // A Decimal128 value renders as a 4×u32 little-endian array in records.
166
191
  // Convert (scale-0 -> integer) to a `bigint`, matching token/exact match.
@@ -247,8 +272,8 @@ class Table {
247
272
  * append == one commit.
248
273
  */
249
274
  append(data) {
250
- const ipc = dataToIpc(data, () => this.schema());
251
- guard(this.remote, () => this.inner.append(ipc));
275
+ const input = writeInput(data);
276
+ guard(this.remote, () => ("rows" in input ? this.inner.appendRows(input.rows) : this.inner.append(input.ipc)));
252
277
  }
253
278
  /**
254
279
  * {@link append}, naming the source the rows came from. The superfiles
@@ -259,8 +284,8 @@ class Table {
259
284
  * a hosted table.
260
285
  */
261
286
  appendNamed(data, sourceName) {
262
- const ipc = dataToIpc(data, () => this.schema());
263
- guard(this.remote, () => this.inner.appendNamed(ipc, sourceName));
287
+ const input = writeInput(data);
288
+ guard(this.remote, () => "rows" in input ? this.inner.appendRowsNamed(input.rows, sourceName) : this.inner.appendNamed(input.ipc, sourceName));
264
289
  }
265
290
  bm25Search(column, query, k, opts = {}) {
266
291
  const buf = guard(this.remote, () => this.inner.bm25Search(column, query, k, opts.mode, opts.stats, opts.projection, opts.k1, opts.b));
@@ -293,8 +318,8 @@ class Table {
293
318
  * `data` (same shapes as `append`), 1:1 — the matched count must equal the
294
319
  * replacement-row count. Requires durable storage (not `memory://`). */
295
320
  update(predicate, data) {
296
- const ipc = dataToIpc(data, () => this.schema());
297
- return guard(this.remote, () => this.inner.update(predicate, ipc));
321
+ const input = writeInput(data);
322
+ return guard(this.remote, () => "rows" in input ? this.inner.updateRows(predicate, input.rows) : this.inner.update(predicate, input.ipc));
298
323
  }
299
324
  /** Delete rows matching a SQL predicate (e.g. `"status = 'spam'"`).
300
325
  * Requires durable storage (not `memory://`). */
@@ -373,6 +398,20 @@ class Connection {
373
398
  openTable(name) {
374
399
  return new Table(guard(this.remote, () => this.inner.openTable(name)), this.remote);
375
400
  }
401
+ /**
402
+ * The schema document of `name`: its fields with ids, types, nullability
403
+ * and indexes, the field cap and the `schema_id`. With `patch` (the same
404
+ * shape), merge it into the schema — or create the table from it when
405
+ * there is none — and return the document afterwards. A field is matched
406
+ * by `id` when it carries one and by `name` otherwise; an unmatched field
407
+ * is added, a different type changes the column, `dropped: true` retires
408
+ * it, and a field not mentioned is untouched. `expectedSchemaId` is a
409
+ * compare-and-set against the current `schema_id`.
410
+ */
411
+ schema(name, patch, expectedSchemaId) {
412
+ const text = guard(this.remote, () => this.inner.schema(name, patch === undefined ? undefined : JSON.stringify(patch), expectedSchemaId));
413
+ return JSON.parse(text);
414
+ }
376
415
  /** Drop a table. `purge` defaults to `true`, which also deletes the table's storage; pass `false` to only unregister it and keep the bytes. */
377
416
  dropTable(name, purge) {
378
417
  guard(this.remote, () => this.inner.dropTable(name, purge));
@@ -130,6 +130,11 @@ export interface ReindexReport {
130
130
  * only re-ingesting them from source can.
131
131
  */
132
132
  unrepairableColumns: Array<string>
133
+ /**
134
+ * Superfiles left untouched because their footer places a blob where
135
+ * the file or its manifest entry contradicts; they need inspection.
136
+ */
137
+ inconsistentFooters: Array<string>
133
138
  }
134
139
  /** What a `reindex` would do, and what it would cost. */
135
140
  export interface StalenessReport {
@@ -146,6 +151,11 @@ export interface StalenessReport {
146
151
  bytesToRewrite: number
147
152
  /** Columns no repair can fix, because their text was never stored. */
148
153
  unrepairableColumns: Array<string>
154
+ /**
155
+ * Superfiles whose footer places a blob where the file or its manifest
156
+ * entry contradicts. A reindex reports these and never rewrites them.
157
+ */
158
+ inconsistentFooters: Array<string>
149
159
  /** Whether a reindex would do nothing at all. */
150
160
  isCurrent: boolean
151
161
  }
@@ -290,6 +300,14 @@ export declare class Connection {
290
300
  createTable(name: string, schema: Buffer, indexes: IndexSpec): Table
291
301
  /** Open an existing table by name. */
292
302
  openTable(name: string): Table
303
+ /**
304
+ * The table's schema document as JSON text, or — with `patch_json`, a
305
+ * patch in the same shape — the document after merging the patch into
306
+ * the schema (creating the table when there is none). The JS wrapper
307
+ * parses the result and stringifies the patch; `expected_schema_id` is
308
+ * a compare-and-set against the current `schema_id`.
309
+ */
310
+ schema(name: string, patchJson?: string | undefined | null, expectedSchemaId?: number | undefined | null): string
293
311
  /**
294
312
  * Drop a table. `purge` defaults to `true`, which also deletes the
295
313
  * table's storage subtree after the catalog commit, reclaiming the bytes;
@@ -303,6 +321,7 @@ export declare class Connection {
303
321
  * the JS side reads with `tableFromIPC`. Search is available in SQL
304
322
  * via the TVFs, e.g.
305
323
  * `SELECT _id, score FROM bm25_search('docs', 'body', 'q', 10)`.
324
+ * Read-only, one statement per call: a write throws `InvalidArg`.
306
325
  */
307
326
  querySql(sql: string): Buffer
308
327
  }
@@ -315,6 +334,24 @@ export declare class Table {
315
334
  * concatenated into one commit; an empty stream is a no-op.
316
335
  */
317
336
  append(data: Buffer): void
337
+ /**
338
+ * Append rows given as a JSON array of objects (the text the JS wrapper
339
+ * produces with `JSON.stringify`): nested objects flatten to dot paths,
340
+ * arrays become list columns, a key the table has not seen adds a
341
+ * column typed from its values, and a value whose type disagrees with
342
+ * the column's is refused.
343
+ */
344
+ appendRows(rowsJson: string): void
345
+ /**
346
+ * `appendRows`, naming the source the rows came from as `appendNamed`
347
+ * does.
348
+ */
349
+ appendRowsNamed(rowsJson: string, sourceName: string): void
350
+ /**
351
+ * `update` with the replacement rows as a JSON array of objects, mapped
352
+ * as `appendRows` maps them.
353
+ */
354
+ updateRows(predicate: string, rowsJson: string): MutationStats
318
355
  /**
319
356
  * `append`, naming the source the rows came from: the superfiles this
320
357
  * commit writes are keyed `data/<stem>-<uuid>.sf.parquet`, with the stem
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infino-ai/infino",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Fast search on object storage — SQL, full-text, and vector search.",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
@@ -82,12 +82,12 @@
82
82
  "apache-arrow": "^17"
83
83
  },
84
84
  "optionalDependencies": {
85
- "infx-darwin-x64": "0.9.0",
86
- "infx-darwin-arm64": "0.9.0",
87
- "infx-linux-x64-gnu": "0.9.0",
88
- "infx-linux-arm64-gnu": "0.9.0",
89
- "infx-linux-x64-musl": "0.9.0",
90
- "infx-linux-arm64-musl": "0.9.0"
85
+ "infx-darwin-x64": "0.9.2",
86
+ "infx-darwin-arm64": "0.9.2",
87
+ "infx-linux-x64-gnu": "0.9.2",
88
+ "infx-linux-arm64-gnu": "0.9.2",
89
+ "infx-linux-x64-musl": "0.9.2",
90
+ "infx-linux-arm64-musl": "0.9.2"
91
91
  },
92
92
  "devDependencies": {
93
93
  "@napi-rs/cli": "^2.18.0",