@peerbit/indexer-sqlite3 1.0.2 → 1.0.3-b57d808

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/src/engine.ts CHANGED
@@ -12,6 +12,7 @@ import type {
12
12
  import * as types from "@peerbit/indexer-interface";
13
13
  import { v4 as uuid } from "uuid";
14
14
  import {
15
+ MissingFieldError,
15
16
  type Table,
16
17
  buildJoin,
17
18
  convertCountRequestToQuery,
@@ -20,13 +21,14 @@ import {
20
21
  /* getTableName, */
21
22
  convertSumRequestToQuery,
22
23
  escapeColumnName,
24
+ generateSelectQuery,
23
25
  getInlineTableFieldName,
24
26
  getSQLTable,
25
27
  getTablePrefixedField,
26
28
  insert,
27
29
  resolveInstanceFromValue,
28
30
  resolveTable,
29
- selectAllFields,
31
+ selectAllFieldsFromTable,
30
32
  selectChildren,
31
33
  } from "./schema.js";
32
34
  import type { Database, Statement } from "./types.js";
@@ -124,10 +126,7 @@ export class SQLLiteIndex<T extends Record<string, any>>
124
126
  throw new Error("Missing schema");
125
127
  }
126
128
 
127
- this.primaryKeyString = getInlineTableFieldName(
128
- this.primaryKeyArr.slice(0, this.primaryKeyArr.length - 1),
129
- this.primaryKeyArr[this.primaryKeyArr.length - 1],
130
- );
129
+ this.primaryKeyString = getInlineTableFieldName(this.primaryKeyArr);
131
130
 
132
131
  return this;
133
132
  }
@@ -152,10 +151,7 @@ export class SQLLiteIndex<T extends Record<string, any>>
152
151
  const tables = getSQLTable(
153
152
  this.properties.schema!,
154
153
  this.scopeString ? [this.scopeString] : [],
155
- getInlineTableFieldName(
156
- this.primaryKeyArr.slice(0, -1),
157
- this.primaryKeyArr[this.primaryKeyArr.length - 1],
158
- ), // TODO fix this, should be array
154
+ getInlineTableFieldName(this.primaryKeyArr), // TODO fix this, should be array
159
155
  false,
160
156
  undefined,
161
157
  false,
@@ -164,10 +160,6 @@ export class SQLLiteIndex<T extends Record<string, any>>
164
160
 
165
161
  this._rootTables = tables.filter((x) => x.parent == null);
166
162
 
167
- if (this._rootTables.length > 1) {
168
- throw new Error("Multiple root tables not supported (yet)");
169
- }
170
-
171
163
  const allTables = tables;
172
164
 
173
165
  for (const table of allTables) {
@@ -284,8 +276,11 @@ export class SQLLiteIndex<T extends Record<string, any>>
284
276
  options?: { shape: Shape },
285
277
  ): Promise<IndexedResult<T> | undefined> {
286
278
  for (const table of this._rootTables) {
287
- const { join: joinMap, query } = selectAllFields(table, options?.shape);
288
- const sql = `${query} ${buildJoin(joinMap, true)} where ${this.primaryKeyString} = ? `;
279
+ const { join: joinMap, selects } = selectAllFieldsFromTable(
280
+ table,
281
+ options?.shape,
282
+ );
283
+ const sql = `${generateSelectQuery(table, selects)} ${buildJoin(joinMap, true)} where ${this.primaryKeyString} = ? `;
289
284
  const stmt = await this.properties.db.prepare(sql);
290
285
  const rows = await stmt.get([id.key]);
291
286
  await stmt.finalize?.();
@@ -390,9 +385,10 @@ export class SQLLiteIndex<T extends Record<string, any>>
390
385
  let results: IndexedResult<T>[] = await Promise.all(
391
386
  allResults.map(async (row: any) => {
392
387
  let selectedTable = this._rootTables.find(
393
- (table) =>
388
+ (table /* row["table_name"] === table.name, */) =>
394
389
  row[getTablePrefixedField(table, this.primaryKeyString)] != null,
395
390
  )!;
391
+
396
392
  const value = await resolveInstanceFromValue<T>(
397
393
  row,
398
394
  this.tables,
@@ -428,6 +424,7 @@ export class SQLLiteIndex<T extends Record<string, any>>
428
424
  }
429
425
  return { results, kept: iterator.kept };
430
426
  };
427
+
431
428
  const iterator = {
432
429
  kept: 0,
433
430
  fetch,
@@ -483,46 +480,104 @@ export class SQLLiteIndex<T extends Record<string, any>>
483
480
 
484
481
  async del(query: types.DeleteRequest): Promise<types.IdKey[]> {
485
482
  let ret: types.IdKey[] = [];
483
+ let once = false;
484
+ let lastError: Error | undefined = undefined;
486
485
  for (const table of this._rootTables) {
487
- const stmt = await this.properties.db.prepare(
488
- convertDeleteRequestToQuery(query, this.tables, table),
489
- );
490
- const results: any[] = await stmt.all([]);
491
- await stmt.finalize?.();
492
- // TODO types
493
- for (const result of results) {
494
- ret.push(types.toId(result[table.primary as string]));
486
+ try {
487
+ const stmt = await this.properties.db.prepare(
488
+ convertDeleteRequestToQuery(query, this.tables, table),
489
+ );
490
+ const results: any[] = await stmt.all([]);
491
+ await stmt.finalize?.();
492
+ // TODO types
493
+ for (const result of results) {
494
+ ret.push(types.toId(result[table.primary as string]));
495
+ }
496
+ once = true;
497
+ } catch (error) {
498
+ if (error instanceof MissingFieldError) {
499
+ lastError = error;
500
+ continue;
501
+ }
502
+
503
+ throw error;
495
504
  }
496
505
  }
506
+
507
+ if (!once) {
508
+ throw lastError;
509
+ }
510
+
497
511
  return ret;
498
512
  }
499
513
 
500
514
  async sum(query: types.SumRequest): Promise<number | bigint> {
501
515
  let ret: number | bigint | undefined = undefined;
516
+ let once = false;
517
+ let lastError: Error | undefined = undefined;
518
+
519
+ let inlinedName = getInlineTableFieldName(query.key);
502
520
  for (const table of this._rootTables) {
503
- const stmt = await this.properties.db.prepare(
504
- convertSumRequestToQuery(query, this.tables, table),
505
- );
506
- const result = await stmt.get();
507
- await stmt.finalize?.();
508
- if (ret == null) {
509
- (ret as any) = result.sum as number;
510
- } else {
511
- (ret as any) += result.sum as number;
521
+ try {
522
+ if (table.fields.find((x) => x.name === inlinedName) == null) {
523
+ lastError = new MissingFieldError(
524
+ "Missing field: " + query.key.join("."),
525
+ );
526
+ continue;
527
+ }
528
+
529
+ const stmt = await this.properties.db.prepare(
530
+ convertSumRequestToQuery(query, this.tables, table),
531
+ );
532
+ const result = await stmt.get();
533
+ await stmt.finalize?.();
534
+ if (ret == null) {
535
+ (ret as any) = result.sum as number;
536
+ } else {
537
+ (ret as any) += result.sum as number;
538
+ }
539
+ once = true;
540
+ } catch (error) {
541
+ if (error instanceof MissingFieldError) {
542
+ lastError = error;
543
+ continue;
544
+ }
545
+ throw error;
512
546
  }
513
547
  }
548
+
549
+ if (!once) {
550
+ throw lastError;
551
+ }
552
+
514
553
  return ret != null ? ret : 0;
515
554
  }
516
555
 
517
556
  async count(request: types.CountRequest): Promise<number> {
518
557
  let ret: number = 0;
558
+ let once = false;
559
+ let lastError: Error | undefined = undefined;
519
560
  for (const table of this._rootTables) {
520
- const stmt = await this.properties.db.prepare(
521
- convertCountRequestToQuery(request, this.tables, table),
522
- );
523
- const result = await stmt.get();
524
- await stmt.finalize?.();
525
- ret += Number(result.count);
561
+ try {
562
+ const stmt = await this.properties.db.prepare(
563
+ convertCountRequestToQuery(request, this.tables, table),
564
+ );
565
+ const result = await stmt.get();
566
+ await stmt.finalize?.();
567
+ ret += Number(result.count);
568
+ once = true;
569
+ } catch (error) {
570
+ if (error instanceof MissingFieldError) {
571
+ lastError = error;
572
+ continue;
573
+ }
574
+
575
+ throw error;
576
+ }
577
+ }
578
+
579
+ if (!once) {
580
+ throw lastError;
526
581
  }
527
582
  return ret;
528
583
  }
package/src/schema.ts CHANGED
@@ -72,6 +72,13 @@ export const convertToSQLType = (
72
72
  const nullAsUndefined = (value: any) => (value === null ? undefined : value);
73
73
  export const escapeColumnName = (name: string) => `"${name}"`;
74
74
 
75
+ export class MissingFieldError extends Error {
76
+ constructor(message: string) {
77
+ super(message);
78
+ this.name = "MissingFieldError";
79
+ }
80
+ }
81
+
75
82
  export const convertFromSQLType = (
76
83
  value: boolean | bigint | string | number | Uint8Array,
77
84
  type?: FieldType,
@@ -814,10 +821,22 @@ export const getTablePrefixedField = (
814
821
  `${skipPrefix ? "" : table.name + "#"}${getInlineTableFieldName(table.path.slice(1), key)}`;
815
822
  export const getTableNameFromPrefixedField = (prefixedField: string) =>
816
823
  prefixedField.split("#")[0];
824
+
817
825
  export const getInlineTableFieldName = (
818
826
  path: string[] | undefined,
819
- key: string,
820
- ) => (path && path.length > 0 ? `${path.join("_")}__${key}` : key);
827
+ key?: string,
828
+ ) => {
829
+ if (key) {
830
+ return path && path.length > 0 ? `${path.join("_")}__${key}` : key;
831
+ } else {
832
+ // last element in the path is the key, the rest is the path
833
+ // join key with __ , rest with _
834
+
835
+ return path!.length > 2
836
+ ? `${path!.slice(0, -1).join("_")}__${path![path!.length - 1]}`
837
+ : path!.join("__");
838
+ }
839
+ };
821
840
 
822
841
  const matchFieldInShape = (
823
842
  shape: types.Shape | undefined,
@@ -851,13 +870,73 @@ const matchFieldInShape = (
851
870
  export const selectChildren = (childrenTable: Table) =>
852
871
  "select * from " + childrenTable.name + " where " + PARENT_TABLE_ID + " = ?";
853
872
 
854
- export const selectAllFields = (
873
+ export const generateSelectQuery = (
874
+ table: Table,
875
+ selects: { from: string; as: string }[],
876
+ ) => {
877
+ return `SELECT ${selects.map((x) => `${x.from} as ${x.as}`).join(", ")} FROM ${table.name}`;
878
+ };
879
+
880
+ export const selectAllFieldsFromTables = (
881
+ tables: Table[],
882
+ shape: types.Shape | undefined,
883
+ ) => {
884
+ const selectsPerTable: {
885
+ selects: {
886
+ from: string;
887
+ as: string;
888
+ }[];
889
+ joins: Map<string, JoinTable>;
890
+ }[] = [];
891
+
892
+ for (const table of tables) {
893
+ const { selects, join: joinFromSelect } = selectAllFieldsFromTable(
894
+ table,
895
+ shape,
896
+ );
897
+ selectsPerTable.push({ selects, joins: joinFromSelect });
898
+ }
899
+
900
+ // pad with empty selects to make sure all selects have the same length
901
+ /* const maxSelects = Math.max(...selectsPerTable.map(x => x.selects.length)); */
902
+
903
+ let newSelects: {
904
+ from: string;
905
+ as: string;
906
+ }[][] = [];
907
+ for (const [i, selects] of selectsPerTable.entries()) {
908
+ const newSelect = [];
909
+ for (const [j, selectsOther] of selectsPerTable.entries()) {
910
+ if (i !== j) {
911
+ for (const select of selectsOther.selects) {
912
+ newSelect.push({ from: "NULL", as: select.as });
913
+ }
914
+ } else {
915
+ selects.selects.forEach((select) => newSelect.push(select));
916
+ }
917
+ }
918
+ newSelects.push(newSelect);
919
+
920
+ /* let pad = 0;
921
+ while (select.selects.length < maxSelects) {
922
+ select.selects.push({ from: "NULL", as: `'pad#${++pad}'` });
923
+ } */
924
+ }
925
+ // also return table name
926
+ for (const [i, selects] of selectsPerTable.entries()) {
927
+ selects.selects = newSelects[i];
928
+ }
929
+
930
+ return selectsPerTable;
931
+ };
932
+
933
+ export const selectAllFieldsFromTable = (
855
934
  table: Table,
856
935
  shape: types.Shape | undefined,
857
936
  ) => {
858
937
  let stack: { table: Table; shape?: types.Shape }[] = [{ table, shape }];
859
938
  let join: Map<string, JoinTable> = new Map();
860
- const fieldResolvers: string[] = [];
939
+ const fieldResolvers: { from: string; as: string }[] = [];
861
940
  for (const tableAndShape of stack) {
862
941
  if (!tableAndShape.table.inline) {
863
942
  for (const field of tableAndShape.table.fields) {
@@ -866,8 +945,10 @@ export const selectAllFields = (
866
945
  !tableAndShape.shape ||
867
946
  matchFieldInShape(tableAndShape.shape, [], field)
868
947
  ) {
869
- const value = `${tableAndShape.table.name}.${escapeColumnName(field.name)} as '${getTablePrefixedField(tableAndShape.table, field.name)}'`;
870
- fieldResolvers.push(value);
948
+ fieldResolvers.push({
949
+ from: `${tableAndShape.table.name}.${escapeColumnName(field.name)}`,
950
+ as: `'${getTablePrefixedField(tableAndShape.table, field.name)}'`,
951
+ });
871
952
  }
872
953
  }
873
954
  }
@@ -903,7 +984,7 @@ export const selectAllFields = (
903
984
  }
904
985
 
905
986
  return {
906
- query: `SELECT ${fieldResolvers.join(", ")} FROM ${table.name}`,
987
+ selects: fieldResolvers, // `SELECT ${fieldResolvers.join(", ")} FROM ${table.name}`,
907
988
  join,
908
989
  };
909
990
  };
@@ -1120,7 +1201,7 @@ export const convertSumRequestToQuery = (
1120
1201
  tables: Map<string, Table>,
1121
1202
  table: Table,
1122
1203
  ) => {
1123
- return `SELECT SUM(${table.name}.${request.key.join(".")}) as sum FROM ${table.name} ${convertRequestToQuery(request, tables, table).query}`;
1204
+ return `SELECT SUM(${table.name}.${getInlineTableFieldName(request.key)}) as sum FROM ${table.name} ${convertRequestToQuery(request, tables, table).query}`;
1124
1205
  };
1125
1206
 
1126
1207
  export const convertCountRequestToQuery = (
@@ -1138,23 +1219,45 @@ export const convertSearchRequestToQuery = (
1138
1219
  shape: types.Shape | undefined,
1139
1220
  ) => {
1140
1221
  let unionBuilder = "";
1141
- let orderByClause: string | undefined = undefined;
1142
- for (const table of rootTables) {
1143
- const { query: selectQuery, join: joinFromSelect } = selectAllFields(
1144
- table,
1145
- shape,
1146
- );
1147
- const { orderBy, query } = convertRequestToQuery(
1148
- request,
1149
- tables,
1150
- table,
1151
- joinFromSelect,
1152
- );
1153
- unionBuilder += `${unionBuilder.length > 0 ? " UNION ALL " : ""} ${selectQuery} ${query}`;
1154
- orderByClause = orderBy?.length > 0 ? orderBy : orderByClause;
1222
+ let orderByClause: string = "";
1223
+
1224
+ let matchedOnce = false;
1225
+ let lastError: Error | undefined = undefined;
1226
+
1227
+ const selectsPerTable = selectAllFieldsFromTables(rootTables, shape);
1228
+
1229
+ for (const [i, table] of rootTables.entries()) {
1230
+ const { selects, joins: joinFromSelect } = selectsPerTable[i];
1231
+ const selectQuery = generateSelectQuery(table, selects);
1232
+ try {
1233
+ const { orderBy, query } = convertRequestToQuery(
1234
+ request,
1235
+ tables,
1236
+ table,
1237
+ joinFromSelect,
1238
+ );
1239
+ unionBuilder += `${unionBuilder.length > 0 ? " UNION ALL " : ""} ${selectQuery} ${query}`;
1240
+ orderByClause =
1241
+ orderBy?.length > 0
1242
+ ? orderByClause.length > 0
1243
+ ? orderByClause + ", " + orderBy
1244
+ : orderBy
1245
+ : orderByClause;
1246
+ matchedOnce = true;
1247
+ } catch (error) {
1248
+ if (error instanceof MissingFieldError) {
1249
+ lastError = error;
1250
+ continue;
1251
+ }
1252
+ throw error;
1253
+ }
1155
1254
  }
1156
1255
 
1157
- return `${unionBuilder} ${orderByClause ? orderByClause : ""} limit ? offset ?`;
1256
+ if (!matchedOnce) {
1257
+ throw lastError;
1258
+ }
1259
+
1260
+ return `${unionBuilder} ${orderByClause ? "ORDER BY " + orderByClause : ""} limit ? offset ?`;
1158
1261
  };
1159
1262
 
1160
1263
  type SearchQueryParts = { query: string; orderBy: string };
@@ -1197,9 +1300,7 @@ const convertRequestToQuery = <
1197
1300
 
1198
1301
  if (request instanceof types.SearchRequest) {
1199
1302
  if (request.sort.length > 0) {
1200
- if (request.sort.length > 0) {
1201
- orderByBuilder = "ORDER BY ";
1202
- }
1303
+ orderByBuilder = "";
1203
1304
  let once = false;
1204
1305
  for (const sort of request.sort) {
1205
1306
  const { foreignTables, queryKey } = resolveTableToQuery(
@@ -1403,10 +1504,7 @@ const resolveTableToQuery = (
1403
1504
  // this means we need to also check if the key is a field in the current table
1404
1505
 
1405
1506
  if (searchSelf) {
1406
- const inlineName = getInlineTableFieldName(
1407
- path.slice(0, -1),
1408
- path[path.length - 1],
1409
- );
1507
+ const inlineName = getInlineTableFieldName(path);
1410
1508
  let field = table.fields.find((x) => x.name === inlineName);
1411
1509
  if (field) {
1412
1510
  return {
@@ -1428,7 +1526,7 @@ const resolveTableToQuery = (
1428
1526
  const field = schema.fields.find((x) => x.key === key)!;
1429
1527
  if (!field && currentTable.children.length > 0) {
1430
1528
  // second arg is needed because of polymorphic fields we might end up here intentially to check what tables to query
1431
- throw new Error(
1529
+ throw new MissingFieldError(
1432
1530
  `Property with key "${key}" is not found in the schema ${JSON.stringify(schema.fields.map((x) => x.key))}`,
1433
1531
  );
1434
1532
  }
@@ -1482,6 +1580,9 @@ const resolveTableToQuery = (
1482
1580
  let foreignTables: JoinTable[] = currentTables.filter((x) =>
1483
1581
  x.table.fields.find((x) => x.key === path[path.length - 1]),
1484
1582
  );
1583
+ if (foreignTables.length === 0) {
1584
+ throw new MissingFieldError("Failed to find field to join");
1585
+ }
1485
1586
  let tableToQuery: Table | undefined =
1486
1587
  foreignTables[foreignTables.length - 1].table;
1487
1588
  let queryKeyPath = [path[path.length - 1]];
@@ -1494,10 +1595,7 @@ const resolveTableToQuery = (
1494
1595
 
1495
1596
  let queryKey =
1496
1597
  queryKeyPath.length > 0
1497
- ? getInlineTableFieldName(
1498
- queryKeyPath.slice(0, -1),
1499
- queryKeyPath[queryKeyPath.length - 1],
1500
- )
1598
+ ? getInlineTableFieldName(queryKeyPath)
1501
1599
  : FOREIGN_VALUE_PROPERTY;
1502
1600
  return { queryKey, foreignTables };
1503
1601
  };
@@ -1511,10 +1609,7 @@ const convertStateFieldQuery = (
1511
1609
  tableAlias: string | undefined = undefined,
1512
1610
  ): { where: string } => {
1513
1611
  // if field id represented as foreign table, do join and compare
1514
- const inlinedName = getInlineTableFieldName(
1515
- query.key.slice(0, query.key.length - 1),
1516
- query.key[query.key.length - 1],
1517
- );
1612
+ const inlinedName = getInlineTableFieldName(query.key);
1518
1613
  const tableField = table.fields.find(
1519
1614
  (x) => x.name === inlinedName,
1520
1615
  ); /* stringArraysEquals(query.key, [...table.parentPath, x.name]) )*/