@nymphjs/driver-postgresql 1.0.0-beta.102 → 1.0.0-beta.104

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/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [1.0.0-beta.104](https://github.com/sciactive/nymphjs/compare/v1.0.0-beta.103...v1.0.0-beta.104) (2025-12-01)
7
+
8
+ ### Features
9
+
10
+ - add support for indexing on specific properties and scopes ([ed3cbf1](https://github.com/sciactive/nymphjs/commit/ed3cbf12ee3d83b750188998501c3c89fd4c040f))
11
+
12
+ # [1.0.0-beta.103](https://github.com/sciactive/nymphjs/compare/v1.0.0-beta.102...v1.0.0-beta.103) (2025-11-30)
13
+
14
+ ### Bug Fixes
15
+
16
+ - handle search clause with only stop words ([c8703ff](https://github.com/sciactive/nymphjs/commit/c8703ff7615cc47b110c535d3a2709375a68bd51))
17
+
6
18
  # [1.0.0-beta.102](https://github.com/sciactive/nymphjs/compare/v1.0.0-beta.101...v1.0.0-beta.102) (2025-11-29)
7
19
 
8
20
  **Note:** Version bump only for package @nymphjs/driver-postgresql
@@ -73,6 +73,17 @@ export default class PostgreSQLDriver extends NymphDriver {
73
73
  commit(name: string): Promise<boolean>;
74
74
  deleteEntityByID(guid: string, className?: EntityConstructor | string | null): Promise<boolean>;
75
75
  deleteUID(name: string): Promise<boolean>;
76
+ getIndexes(etype: string): Promise<{
77
+ scope: "data" | "references" | "tokens";
78
+ name: string;
79
+ property: string;
80
+ }[]>;
81
+ addIndex(etype: string, definition: {
82
+ scope: 'data' | 'references' | 'tokens';
83
+ name: string;
84
+ property: string;
85
+ }): Promise<boolean>;
86
+ deleteIndex(etype: string, scope: 'data' | 'references' | 'tokens', name: string): Promise<boolean>;
76
87
  getEtypes(): Promise<string[]>;
77
88
  exportDataIterator(): AsyncGenerator<{
78
89
  type: 'comment' | 'uid' | 'entity';
@@ -259,9 +259,9 @@ export default class PostgreSQLDriver extends NymphDriver {
259
259
  await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference_guid_name`)};`, { connection });
260
260
  await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference_guid_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("reference", "guid", "name");`, { connection });
261
261
  await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid_reference_nameuser`)};`, { connection });
262
- await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid_reference_nameuser`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("guid", "reference") WHERE "name"='user';`, { connection });
263
- await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid_reference_namegroup`)};`, { connection });
264
- await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid_reference_namegroup`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("guid", "reference") WHERE "name"='group';`, { connection });
262
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid_reference_nameuser`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("reference", "guid") WHERE "name"='user';`, { connection });
263
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference_guid_namegroup`)};`, { connection });
264
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference_guid_namegroup`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("reference", "guid") WHERE "name"='group';`, { connection });
265
265
  await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} SET ( autovacuum_vacuum_scale_factor = 0.05, autovacuum_analyze_scale_factor = 0.05 );`, { connection });
266
266
  }
267
267
  async createTokensTable(etype, connection) {
@@ -355,14 +355,14 @@ export default class PostgreSQLDriver extends NymphDriver {
355
355
  return await runQuery();
356
356
  }
357
357
  catch (e2) {
358
- throw new QueryFailedError('Query failed: ' + e2?.code + ' - ' + e2?.message, query);
358
+ throw new QueryFailedError('Query failed: ' + e2?.code + ' - ' + e2?.message, query, e2?.code);
359
359
  }
360
360
  }
361
361
  else if (errorCode === '23505') {
362
362
  throw new EntityUniqueConstraintError(`Unique constraint violation.`);
363
363
  }
364
364
  else {
365
- throw new QueryFailedError('Query failed: ' + e?.code + ' - ' + e?.message, query);
365
+ throw new QueryFailedError('Query failed: ' + e?.code + ' - ' + e?.message, query, e?.code);
366
366
  }
367
367
  }
368
368
  }
@@ -526,6 +526,67 @@ export default class PostgreSQLDriver extends NymphDriver {
526
526
  });
527
527
  return true;
528
528
  }
529
+ async getIndexes(etype) {
530
+ const indexes = [];
531
+ for (let [scope, suffix] of [
532
+ ['data', '_json'],
533
+ ['references', '_reference_guid'],
534
+ ['tokens', '_token_position_stem'],
535
+ ]) {
536
+ const indexDefinitions = await this.queryArray(`SELECT * FROM "pg_indexes" WHERE "indexname" LIKE @pattern;`, {
537
+ params: {
538
+ pattern: `${this.prefix}${scope}_${etype}_id_custom_%${suffix}`,
539
+ },
540
+ });
541
+ for (const indexDefinition of indexDefinitions) {
542
+ indexes.push({
543
+ scope,
544
+ name: indexDefinition.indexname.substring(`${this.prefix}${scope}_${etype}_id_custom_`.length, indexDefinition.indexname.length - suffix.length),
545
+ property: (indexDefinition.indexdef.match(/WHERE\s+\(?\s*name\s*=\s*'(.*)'/) ?? [])[1] ?? '',
546
+ });
547
+ }
548
+ }
549
+ return indexes;
550
+ }
551
+ async addIndex(etype, definition) {
552
+ this.checkIndexName(definition.name);
553
+ await this.deleteIndex(etype, definition.scope, definition.name);
554
+ const connection = await this.getConnection(true);
555
+ if (definition.scope === 'data') {
556
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${definition.name}_json`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING gin ("json") WHERE "name"=${PostgreSQLDriver.escapeValue(definition.property)};`, { connection });
557
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${definition.name}_string_gin`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING gin ("string" gin_trgm_ops) WHERE "name"=${PostgreSQLDriver.escapeValue(definition.property)};`, { connection });
558
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${definition.name}_string_btree`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree (LEFT("string", 1024)) WHERE "name"=${PostgreSQLDriver.escapeValue(definition.property)};`, { connection });
559
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${definition.name}_number`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("number") WHERE "name"=${PostgreSQLDriver.escapeValue(definition.property)};`, { connection });
560
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${definition.name}_truthy`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("truthy") WHERE "name"=${PostgreSQLDriver.escapeValue(definition.property)};`, { connection });
561
+ }
562
+ else if (definition.scope === 'references') {
563
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_custom_${definition.name}_reference_guid`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("reference", "guid") WHERE "name"=${PostgreSQLDriver.escapeValue(definition.property)};`, { connection });
564
+ }
565
+ else if (definition.scope === 'tokens') {
566
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}_id_custom_${definition.name}_token_position_stem`)} ON ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} USING btree ("token", "position", "stem") WHERE "name"=${PostgreSQLDriver.escapeValue(definition.property)};`, { connection });
567
+ }
568
+ connection.done();
569
+ return true;
570
+ }
571
+ async deleteIndex(etype, scope, name) {
572
+ this.checkIndexName(name);
573
+ const connection = await this.getConnection(true);
574
+ if (scope === 'data') {
575
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${name}_json`)};`, { connection });
576
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${name}_string_gin`)};`, { connection });
577
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${name}_string_btree`)};`, { connection });
578
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${name}_number`)};`, { connection });
579
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${name}_truthy`)};`, { connection });
580
+ }
581
+ else if (scope === 'references') {
582
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_custom_${name}_reference_guid`)};`, { connection });
583
+ }
584
+ else if (scope === 'tokens') {
585
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}_id_custom_${name}_token_position_stem`)};`, { connection });
586
+ }
587
+ connection.done();
588
+ return true;
589
+ }
529
590
  async getEtypes() {
530
591
  const tables = await this.queryArray('SELECT "table_name" AS "table_name" FROM "information_schema"."tables" WHERE "table_catalog"=@db AND "table_schema"=\'public\' AND "table_name" LIKE @prefix;', {
531
592
  params: {
@@ -921,109 +982,115 @@ export default class PostgreSQLDriver extends NymphDriver {
921
982
  if (curQuery) {
922
983
  curQuery += typeIsOr ? ' OR ' : ' AND ';
923
984
  }
924
- const name = `param${++count.i}`;
925
- const queryPartToken = (term) => {
926
- const value = `param${++count.i}`;
927
- params[value] = term.token;
928
- return ('EXISTS (SELECT "guid" FROM ' +
929
- PostgreSQLDriver.escape(this.prefix + 'tokens_' + etype) +
930
- ' WHERE "guid"=' +
931
- ieTable +
932
- '."guid" AND "name"=@' +
933
- name +
934
- ' AND "token"=@' +
935
- value +
936
- (term.nostemmed ? ' AND "stem"=FALSE' : '') +
937
- ')');
938
- };
939
- const queryPartSeries = (series) => {
940
- const tokenTableSuffix = makeTableSuffix();
941
- const tokenParts = series.tokens.map((token, i) => {
985
+ const parsedFTSQuery = this.tokenizer.parseSearchQuery(curValue[1]);
986
+ if (!parsedFTSQuery.length) {
987
+ curQuery +=
988
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') + '(FALSE)';
989
+ }
990
+ else {
991
+ const name = `param${++count.i}`;
992
+ const queryPartToken = (term) => {
942
993
  const value = `param${++count.i}`;
943
- params[value] = token.token;
944
- return {
945
- fromClause: i === 0
946
- ? 'FROM ' +
947
- PostgreSQLDriver.escape(this.prefix + 'tokens_' + etype) +
948
- ' t' +
949
- tokenTableSuffix +
950
- '0'
951
- : 'JOIN ' +
952
- PostgreSQLDriver.escape(this.prefix + 'tokens_' + etype) +
953
- ' t' +
954
- tokenTableSuffix +
955
- i +
956
- ' ON t' +
957
- tokenTableSuffix +
958
- i +
959
- '."guid" = t' +
960
- tokenTableSuffix +
961
- '0."guid" AND t' +
962
- tokenTableSuffix +
963
- i +
964
- '."name" = t' +
965
- tokenTableSuffix +
966
- '0."name" AND t' +
994
+ params[value] = term.token;
995
+ return ('EXISTS (SELECT "guid" FROM ' +
996
+ PostgreSQLDriver.escape(this.prefix + 'tokens_' + etype) +
997
+ ' WHERE "guid"=' +
998
+ ieTable +
999
+ '."guid" AND "name"=@' +
1000
+ name +
1001
+ ' AND "token"=@' +
1002
+ value +
1003
+ (term.nostemmed ? ' AND "stem"=FALSE' : '') +
1004
+ ')');
1005
+ };
1006
+ const queryPartSeries = (series) => {
1007
+ const tokenTableSuffix = makeTableSuffix();
1008
+ const tokenParts = series.tokens.map((token, i) => {
1009
+ const value = `param${++count.i}`;
1010
+ params[value] = token.token;
1011
+ return {
1012
+ fromClause: i === 0
1013
+ ? 'FROM ' +
1014
+ PostgreSQLDriver.escape(this.prefix + 'tokens_' + etype) +
1015
+ ' t' +
1016
+ tokenTableSuffix +
1017
+ '0'
1018
+ : 'JOIN ' +
1019
+ PostgreSQLDriver.escape(this.prefix + 'tokens_' + etype) +
1020
+ ' t' +
1021
+ tokenTableSuffix +
1022
+ i +
1023
+ ' ON t' +
1024
+ tokenTableSuffix +
1025
+ i +
1026
+ '."guid" = t' +
1027
+ tokenTableSuffix +
1028
+ '0."guid" AND t' +
1029
+ tokenTableSuffix +
1030
+ i +
1031
+ '."name" = t' +
1032
+ tokenTableSuffix +
1033
+ '0."name" AND t' +
1034
+ tokenTableSuffix +
1035
+ i +
1036
+ '."position" = t' +
1037
+ tokenTableSuffix +
1038
+ '0."position" + ' +
1039
+ i,
1040
+ whereClause: 't' +
967
1041
  tokenTableSuffix +
968
1042
  i +
969
- '."position" = t' +
970
- tokenTableSuffix +
971
- '0."position" + ' +
972
- i,
973
- whereClause: 't' +
974
- tokenTableSuffix +
975
- i +
976
- '."token"=@' +
977
- value +
978
- (token.nostemmed
979
- ? ' AND t' + tokenTableSuffix + i + '."stem"=FALSE'
980
- : ''),
981
- };
982
- });
983
- return ('EXISTS (SELECT t' +
984
- tokenTableSuffix +
985
- '0."guid" ' +
986
- tokenParts.map((part) => part.fromClause).join(' ') +
987
- ' WHERE t' +
988
- tokenTableSuffix +
989
- '0."guid"=' +
990
- ieTable +
991
- '."guid" AND t' +
992
- tokenTableSuffix +
993
- '0."name"=@' +
994
- name +
995
- ' AND ' +
996
- tokenParts.map((part) => part.whereClause).join(' AND ') +
997
- ')');
998
- };
999
- const queryPartTerm = (term) => {
1000
- if (term.type === 'series') {
1001
- return queryPartSeries(term);
1002
- }
1003
- else if (term.type === 'not') {
1004
- return 'NOT ' + queryPartTerm(term.operand);
1005
- }
1006
- else if (term.type === 'or') {
1007
- let queryParts = [];
1008
- for (let operand of term.operands) {
1009
- queryParts.push(queryPartTerm(operand));
1043
+ '."token"=@' +
1044
+ value +
1045
+ (token.nostemmed
1046
+ ? ' AND t' + tokenTableSuffix + i + '."stem"=FALSE'
1047
+ : ''),
1048
+ };
1049
+ });
1050
+ return ('EXISTS (SELECT t' +
1051
+ tokenTableSuffix +
1052
+ '0."guid" ' +
1053
+ tokenParts.map((part) => part.fromClause).join(' ') +
1054
+ ' WHERE t' +
1055
+ tokenTableSuffix +
1056
+ '0."guid"=' +
1057
+ ieTable +
1058
+ '."guid" AND t' +
1059
+ tokenTableSuffix +
1060
+ '0."name"=@' +
1061
+ name +
1062
+ ' AND ' +
1063
+ tokenParts.map((part) => part.whereClause).join(' AND ') +
1064
+ ')');
1065
+ };
1066
+ const queryPartTerm = (term) => {
1067
+ if (term.type === 'series') {
1068
+ return queryPartSeries(term);
1010
1069
  }
1011
- return '(' + queryParts.join(' OR ') + ')';
1070
+ else if (term.type === 'not') {
1071
+ return 'NOT ' + queryPartTerm(term.operand);
1072
+ }
1073
+ else if (term.type === 'or') {
1074
+ let queryParts = [];
1075
+ for (let operand of term.operands) {
1076
+ queryParts.push(queryPartTerm(operand));
1077
+ }
1078
+ return '(' + queryParts.join(' OR ') + ')';
1079
+ }
1080
+ return queryPartToken(term);
1081
+ };
1082
+ // Run through the query and add terms.
1083
+ let termStrings = [];
1084
+ for (let term of parsedFTSQuery) {
1085
+ termStrings.push(queryPartTerm(term));
1012
1086
  }
1013
- return queryPartToken(term);
1014
- };
1015
- const parsedFTSQuery = this.tokenizer.parseSearchQuery(curValue[1]);
1016
- // Run through the query and add terms.
1017
- let termStrings = [];
1018
- for (let term of parsedFTSQuery) {
1019
- termStrings.push(queryPartTerm(term));
1087
+ curQuery +=
1088
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1089
+ '(' +
1090
+ termStrings.join(' AND ') +
1091
+ ')';
1092
+ params[name] = curValue[0];
1020
1093
  }
1021
- curQuery +=
1022
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1023
- '(' +
1024
- termStrings.join(' AND ') +
1025
- ')';
1026
- params[name] = curValue[0];
1027
1094
  }
1028
1095
  break;
1029
1096
  case 'match':