@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 +12 -0
- package/dist/PostgreSQLDriver.d.ts +11 -0
- package/dist/PostgreSQLDriver.js +169 -102
- package/dist/PostgreSQLDriver.js.map +1 -1
- package/package.json +4 -4
- package/src/PostgreSQLDriver.ts +304 -123
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';
|
package/dist/PostgreSQLDriver.js
CHANGED
|
@@ -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 ("
|
|
263
|
-
await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}
|
|
264
|
-
await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}
|
|
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
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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] =
|
|
944
|
-
return
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
'."
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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':
|