prostgles-server 4.2.544 → 4.2.546
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/dist/DboBuilder/DboBuilder.js.map +1 -1
- package/dist/DboBuilder/QueryBuilder/Functions/Functions.d.ts.map +1 -1
- package/dist/DboBuilder/QueryBuilder/Functions/Functions.js +19 -376
- package/dist/DboBuilder/QueryBuilder/Functions/Functions.js.map +1 -1
- package/dist/DboBuilder/QueryBuilder/Functions/HASHING_FUNCTIONS.d.ts +3 -0
- package/dist/DboBuilder/QueryBuilder/Functions/HASHING_FUNCTIONS.d.ts.map +1 -0
- package/dist/DboBuilder/QueryBuilder/Functions/HASHING_FUNCTIONS.js +107 -0
- package/dist/DboBuilder/QueryBuilder/Functions/HASHING_FUNCTIONS.js.map +1 -0
- package/dist/DboBuilder/QueryBuilder/Functions/TEXT_FUNCTIONS.d.ts +3 -0
- package/dist/DboBuilder/QueryBuilder/Functions/TEXT_FUNCTIONS.d.ts.map +1 -0
- package/dist/DboBuilder/QueryBuilder/Functions/TEXT_FUNCTIONS.js +276 -0
- package/dist/DboBuilder/QueryBuilder/Functions/TEXT_FUNCTIONS.js.map +1 -0
- package/dist/DboBuilder/QueryBuilder/Functions/utils.d.ts +3 -0
- package/dist/DboBuilder/QueryBuilder/Functions/utils.d.ts.map +1 -0
- package/dist/DboBuilder/QueryBuilder/Functions/utils.js +6 -0
- package/dist/DboBuilder/QueryBuilder/Functions/utils.js.map +1 -0
- package/dist/DboBuilder/TableHandler/update.js.map +1 -1
- package/dist/DboBuilder/ViewHandler/getExistsCondition.js +5 -5
- package/dist/DboBuilder/ViewHandler/getExistsCondition.js.map +1 -1
- package/dist/DboBuilder/ViewHandler/parseFieldFilter.js.map +1 -1
- package/dist/DboBuilder/runSql/runSQL.js +1 -1
- package/dist/DboBuilder/runSql/runSQL.js.map +1 -1
- package/dist/FileManager/FileManager.js.map +1 -1
- package/dist/Logging.d.ts +1 -1
- package/dist/Logging.d.ts.map +1 -1
- package/dist/PubSubManager/PubSubManager.d.ts +0 -1
- package/dist/PubSubManager/PubSubManager.d.ts.map +1 -1
- package/dist/PubSubManager/PubSubManager.js.map +1 -1
- package/dist/PubSubManager/SyncReplication/getSyncUtilFunctions.d.ts +7 -2
- package/dist/PubSubManager/SyncReplication/getSyncUtilFunctions.d.ts.map +1 -1
- package/dist/PubSubManager/SyncReplication/getSyncUtilFunctions.js +21 -16
- package/dist/PubSubManager/SyncReplication/getSyncUtilFunctions.js.map +1 -1
- package/dist/PubSubManager/SyncReplication/syncData.d.ts.map +1 -1
- package/dist/PubSubManager/SyncReplication/syncData.js +16 -14
- package/dist/PubSubManager/SyncReplication/syncData.js.map +1 -1
- package/dist/TableConfig/TableConfig.js.map +1 -1
- package/dist/runClientRequest.js +1 -1
- package/dist/runClientRequest.js.map +1 -1
- package/lib/DboBuilder/DboBuilder.ts +1 -1
- package/lib/DboBuilder/QueryBuilder/Functions/Functions.ts +142 -557
- package/lib/DboBuilder/QueryBuilder/Functions/HASHING_FUNCTIONS.ts +120 -0
- package/lib/DboBuilder/QueryBuilder/Functions/TEXT_FUNCTIONS.ts +302 -0
- package/lib/DboBuilder/QueryBuilder/Functions/utils.ts +3 -0
- package/lib/DboBuilder/TableHandler/update.ts +1 -1
- package/lib/DboBuilder/ViewHandler/getExistsCondition.ts +4 -4
- package/lib/DboBuilder/ViewHandler/parseFieldFilter.ts +1 -1
- package/lib/DboBuilder/runSql/runSQL.ts +1 -1
- package/lib/FileManager/FileManager.ts +1 -1
- package/lib/Logging.ts +1 -1
- package/lib/PubSubManager/PubSubManager.ts +0 -1
- package/lib/PubSubManager/SyncReplication/getSyncUtilFunctions.ts +28 -19
- package/lib/PubSubManager/SyncReplication/syncData.ts +16 -14
- package/lib/TableConfig/TableConfig.ts +1 -1
- package/lib/runClientRequest.ts +1 -1
- package/package.json +10 -10
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import type { ColumnInfo, PG_COLUMN_UDT_DATA_TYPE } from "prostgles-types";
|
|
2
|
-
import {
|
|
3
|
-
asName,
|
|
4
|
-
includes,
|
|
5
|
-
isEmpty,
|
|
6
|
-
isObject,
|
|
7
|
-
TextFilter_FullTextSearchFilterKeys,
|
|
8
|
-
postgresToTsType,
|
|
9
|
-
} from "prostgles-types";
|
|
10
|
-
import { parseFieldFilter } from "../../ViewHandler/parseFieldFilter";
|
|
11
1
|
import * as pgPromise from "pg-promise";
|
|
2
|
+
import type { ColumnInfo, PG_COLUMN_UDT_DATA_TYPE } from "prostgles-types";
|
|
3
|
+
import { asName, includes, isObject, TextFilter_FullTextSearchFilterKeys } from "prostgles-types";
|
|
12
4
|
import { asNameAlias } from "../../../utils/asNameAlias";
|
|
5
|
+
import { HASHING_FUNCTIONS } from "./HASHING_FUNCTIONS";
|
|
6
|
+
import { TEXT_FUNCTIONS } from "./TEXT_FUNCTIONS";
|
|
7
|
+
import { asFunction } from "./utils";
|
|
13
8
|
const pgp = pgPromise();
|
|
14
9
|
|
|
15
10
|
type GetQueryArgs = {
|
|
@@ -74,7 +69,6 @@ export type FunctionSpec = {
|
|
|
74
69
|
returnType?: PG_COLUMN_UDT_DATA_TYPE;
|
|
75
70
|
};
|
|
76
71
|
|
|
77
|
-
const MAX_COL_NUM = 1600;
|
|
78
72
|
const asValue = (v: any, castAs = "") => pgp.as.format("$1" + castAs, [v]);
|
|
79
73
|
|
|
80
74
|
const parseUnix = (
|
|
@@ -195,7 +189,7 @@ const JSON_Funcs: FunctionSpec[] = [
|
|
|
195
189
|
const escapedName = asNameAlias(colName, tableAlias);
|
|
196
190
|
return `${name}(${escapedName})`;
|
|
197
191
|
},
|
|
198
|
-
})
|
|
192
|
+
}) satisfies FunctionSpec,
|
|
199
193
|
),
|
|
200
194
|
];
|
|
201
195
|
|
|
@@ -494,116 +488,7 @@ PostGIS_Funcs = PostGIS_Funcs.concat(
|
|
|
494
488
|
* Each function expects a column at the very least
|
|
495
489
|
*/
|
|
496
490
|
export const FUNCTIONS: FunctionSpec[] = [
|
|
497
|
-
|
|
498
|
-
{
|
|
499
|
-
name: "$md5_multi",
|
|
500
|
-
description: ` :[...column_names] -> md5 hash of the column content`,
|
|
501
|
-
type: "function",
|
|
502
|
-
singleColArg: false,
|
|
503
|
-
numArgs: MAX_COL_NUM,
|
|
504
|
-
getFields: (args: any[]) => args,
|
|
505
|
-
getQuery: ({ args, tableAliasRaw: tableAlias }) => {
|
|
506
|
-
const q = pgp.as.format(
|
|
507
|
-
"md5(" +
|
|
508
|
-
args
|
|
509
|
-
.map((fname) => "COALESCE( " + asNameAlias(fname, tableAlias) + "::text, '' )")
|
|
510
|
-
.join(" || ") +
|
|
511
|
-
")",
|
|
512
|
-
);
|
|
513
|
-
return q;
|
|
514
|
-
},
|
|
515
|
-
},
|
|
516
|
-
{
|
|
517
|
-
name: "$md5_multi_agg",
|
|
518
|
-
description: ` :[...column_names] -> md5 hash of the string aggregation of column content`,
|
|
519
|
-
type: "aggregation",
|
|
520
|
-
singleColArg: false,
|
|
521
|
-
numArgs: MAX_COL_NUM,
|
|
522
|
-
getFields: (args: any[]) => args,
|
|
523
|
-
getQuery: ({ args, tableAliasRaw: tableAlias }) => {
|
|
524
|
-
const q = pgp.as.format(
|
|
525
|
-
"md5(string_agg(" +
|
|
526
|
-
args
|
|
527
|
-
.map((fname) => "COALESCE( " + asNameAlias(fname, tableAlias) + "::text, '' )")
|
|
528
|
-
.join(" || ") +
|
|
529
|
-
", ','))",
|
|
530
|
-
);
|
|
531
|
-
return q;
|
|
532
|
-
},
|
|
533
|
-
},
|
|
534
|
-
|
|
535
|
-
{
|
|
536
|
-
name: "$sha256_multi",
|
|
537
|
-
description: ` :[...column_names] -> sha256 hash of the of column content`,
|
|
538
|
-
type: "function",
|
|
539
|
-
singleColArg: false,
|
|
540
|
-
numArgs: MAX_COL_NUM,
|
|
541
|
-
getFields: (args: any[]) => args,
|
|
542
|
-
getQuery: ({ args, tableAliasRaw: tableAlias }) => {
|
|
543
|
-
const q = pgp.as.format(
|
|
544
|
-
"encode(sha256((" +
|
|
545
|
-
args
|
|
546
|
-
.map((fname) => "COALESCE( " + asNameAlias(fname, tableAlias) + ", '' )")
|
|
547
|
-
.join(" || ") +
|
|
548
|
-
")::text::bytea), 'hex')",
|
|
549
|
-
);
|
|
550
|
-
return q;
|
|
551
|
-
},
|
|
552
|
-
},
|
|
553
|
-
{
|
|
554
|
-
name: "$sha256_multi_agg",
|
|
555
|
-
description: ` :[...column_names] -> sha256 hash of the string aggregation of column content`,
|
|
556
|
-
type: "aggregation",
|
|
557
|
-
singleColArg: false,
|
|
558
|
-
numArgs: MAX_COL_NUM,
|
|
559
|
-
getFields: (args: any[]) => args,
|
|
560
|
-
getQuery: ({ args, tableAliasRaw: tableAlias }) => {
|
|
561
|
-
const q = pgp.as.format(
|
|
562
|
-
"encode(sha256(string_agg(" +
|
|
563
|
-
args
|
|
564
|
-
.map((fname) => "COALESCE( " + asNameAlias(fname, tableAlias) + ", '' )")
|
|
565
|
-
.join(" || ") +
|
|
566
|
-
", ',')::text::bytea), 'hex')",
|
|
567
|
-
);
|
|
568
|
-
return q;
|
|
569
|
-
},
|
|
570
|
-
},
|
|
571
|
-
{
|
|
572
|
-
name: "$sha512_multi",
|
|
573
|
-
description: ` :[...column_names] -> sha512 hash of the of column content`,
|
|
574
|
-
type: "function",
|
|
575
|
-
singleColArg: false,
|
|
576
|
-
numArgs: MAX_COL_NUM,
|
|
577
|
-
getFields: (args: any[]) => args,
|
|
578
|
-
getQuery: ({ args, tableAliasRaw: tableAlias }) => {
|
|
579
|
-
const q = pgp.as.format(
|
|
580
|
-
"encode(sha512((" +
|
|
581
|
-
args
|
|
582
|
-
.map((fname) => "COALESCE( " + asNameAlias(fname, tableAlias) + ", '' )")
|
|
583
|
-
.join(" || ") +
|
|
584
|
-
")::text::bytea), 'hex')",
|
|
585
|
-
);
|
|
586
|
-
return q;
|
|
587
|
-
},
|
|
588
|
-
},
|
|
589
|
-
{
|
|
590
|
-
name: "$sha512_multi_agg",
|
|
591
|
-
description: ` :[...column_names] -> sha512 hash of the string aggregation of column content`,
|
|
592
|
-
type: "aggregation",
|
|
593
|
-
singleColArg: false,
|
|
594
|
-
numArgs: MAX_COL_NUM,
|
|
595
|
-
getFields: (args: any[]) => args,
|
|
596
|
-
getQuery: ({ args, tableAliasRaw: tableAlias }) => {
|
|
597
|
-
const q = pgp.as.format(
|
|
598
|
-
"encode(sha512(string_agg(" +
|
|
599
|
-
args
|
|
600
|
-
.map((fname) => "COALESCE( " + asNameAlias(fname, tableAlias) + ", '' )")
|
|
601
|
-
.join(" || ") +
|
|
602
|
-
", ',')::text::bytea), 'hex')",
|
|
603
|
-
);
|
|
604
|
-
return q;
|
|
605
|
-
},
|
|
606
|
-
},
|
|
491
|
+
...HASHING_FUNCTIONS,
|
|
607
492
|
|
|
608
493
|
...FTS_Funcs,
|
|
609
494
|
|
|
@@ -611,78 +496,7 @@ export const FUNCTIONS: FunctionSpec[] = [
|
|
|
611
496
|
|
|
612
497
|
...PostGIS_Funcs,
|
|
613
498
|
|
|
614
|
-
|
|
615
|
-
name: "$left",
|
|
616
|
-
description: ` :[column_name, number] -> substring`,
|
|
617
|
-
type: "function",
|
|
618
|
-
numArgs: 2,
|
|
619
|
-
singleColArg: false,
|
|
620
|
-
getFields: (args: any[]) => [args[0]],
|
|
621
|
-
getQuery: ({ allowedFields, args, tableAliasRaw: tableAlias }) => {
|
|
622
|
-
return pgp.as.format("LEFT(" + asNameAlias(args[0], tableAlias) + ", $1)", [args[1]]);
|
|
623
|
-
},
|
|
624
|
-
},
|
|
625
|
-
{
|
|
626
|
-
name: "$column",
|
|
627
|
-
description: ` :[column_name] -> Returns the column value as is`,
|
|
628
|
-
type: "function",
|
|
629
|
-
numArgs: 1,
|
|
630
|
-
singleColArg: false,
|
|
631
|
-
getFields: (args: any[]) => [args[0]],
|
|
632
|
-
getQuery: ({ allowedFields, args, tableAliasRaw: tableAlias }) => {
|
|
633
|
-
const aliasedColumnName = args[0];
|
|
634
|
-
if (!aliasedColumnName) {
|
|
635
|
-
throw `$column: column_name is required`;
|
|
636
|
-
}
|
|
637
|
-
return pgp.as.format(asNameAlias(aliasedColumnName, tableAlias));
|
|
638
|
-
},
|
|
639
|
-
},
|
|
640
|
-
{
|
|
641
|
-
name: "$unnest_words",
|
|
642
|
-
description: ` :[column_name] -> Splits string at spaces`,
|
|
643
|
-
type: "function",
|
|
644
|
-
numArgs: 1,
|
|
645
|
-
singleColArg: true,
|
|
646
|
-
getFields: (args: any[]) => [args[0]],
|
|
647
|
-
getQuery: ({ allowedFields, args, tableAliasRaw: tableAlias }) => {
|
|
648
|
-
return pgp.as.format(
|
|
649
|
-
"unnest(string_to_array(" + asNameAlias(args[0], tableAlias) + "::TEXT , ' '))",
|
|
650
|
-
); //, [args[1]]
|
|
651
|
-
},
|
|
652
|
-
},
|
|
653
|
-
{
|
|
654
|
-
name: "$right",
|
|
655
|
-
description: ` :[column_name, number] -> substring`,
|
|
656
|
-
type: "function",
|
|
657
|
-
numArgs: 2,
|
|
658
|
-
singleColArg: false,
|
|
659
|
-
getFields: (args: any[]) => [args[0]],
|
|
660
|
-
getQuery: ({ allowedFields, args, tableAliasRaw: tableAlias }) => {
|
|
661
|
-
return pgp.as.format("RIGHT(" + asNameAlias(args[0], tableAlias) + ", $1)", [args[1]]);
|
|
662
|
-
},
|
|
663
|
-
},
|
|
664
|
-
|
|
665
|
-
{
|
|
666
|
-
name: "$to_char",
|
|
667
|
-
type: "function",
|
|
668
|
-
description: ` :[column_name, format<string>] -> format dates and strings. Eg: [current_timestamp, 'HH12:MI:SS']`,
|
|
669
|
-
singleColArg: false,
|
|
670
|
-
numArgs: 2,
|
|
671
|
-
getFields: (args: any[]) => [args[0]],
|
|
672
|
-
getQuery: ({ allowedFields, args, tableAliasRaw: tableAlias }) => {
|
|
673
|
-
if (args.length === 3) {
|
|
674
|
-
return pgp.as.format("to_char(" + asNameAlias(args[0], tableAlias) + ", $2, $3)", [
|
|
675
|
-
args[0],
|
|
676
|
-
args[1],
|
|
677
|
-
args[2],
|
|
678
|
-
]);
|
|
679
|
-
}
|
|
680
|
-
return pgp.as.format("to_char(" + asNameAlias(args[0], tableAlias) + ", $2)", [
|
|
681
|
-
args[0],
|
|
682
|
-
args[1],
|
|
683
|
-
]);
|
|
684
|
-
},
|
|
685
|
-
},
|
|
499
|
+
...TEXT_FUNCTIONS,
|
|
686
500
|
|
|
687
501
|
/**
|
|
688
502
|
* Date trunc utils
|
|
@@ -736,64 +550,62 @@ export const FUNCTIONS: FunctionSpec[] = [
|
|
|
736
550
|
{ val: 5, unit: "millisecond" },
|
|
737
551
|
{ val: 2, unit: "millisecond" },
|
|
738
552
|
])
|
|
739
|
-
.map(
|
|
740
|
-
({
|
|
741
|
-
(
|
|
742
|
-
name: "$date_trunc_" + (val || "") + unit,
|
|
743
|
-
type: "function",
|
|
744
|
-
description: ` :[column_name, opts?: { timeZone: true | 'TZ Name' }] -> round down timestamp to closest ${val || ""} ${unit} `,
|
|
745
|
-
singleColArg: true,
|
|
746
|
-
numArgs: 2,
|
|
747
|
-
getFields: (args: any[]) => [args[0]],
|
|
748
|
-
getQuery: ({ allColumns, args, tableAliasRaw: tableAlias }) => {
|
|
749
|
-
/** Timestamp added to ensure filters work correctly (psql will loose the string value timezone when comparing to a non tz column) */
|
|
750
|
-
const col = parseUnix(args[0], tableAlias, allColumns, args[1]);
|
|
751
|
-
if (!val) return `date_trunc(${asValue(unit)}, ${col})`;
|
|
752
|
-
const PreviousUnit = {
|
|
753
|
-
year: "decade",
|
|
754
|
-
month: "year",
|
|
755
|
-
hour: "day",
|
|
756
|
-
minute: "hour",
|
|
757
|
-
second: "minute",
|
|
758
|
-
millisecond: "second",
|
|
759
|
-
microsecond: "millisecond",
|
|
760
|
-
};
|
|
761
|
-
|
|
762
|
-
const prevUnit = PreviousUnit[unit as "month"];
|
|
763
|
-
if (!prevUnit) {
|
|
764
|
-
throw "Not supported. prevUnit not found";
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
let extractedUnit = `date_part(${asValue(unit, "::text")}, ${col})::int`;
|
|
768
|
-
if (unit === "microsecond" || unit === "millisecond") {
|
|
769
|
-
extractedUnit = `(${extractedUnit} - 1000 * floor(${extractedUnit}/1000)::int)`;
|
|
770
|
-
}
|
|
771
|
-
const res = `(date_trunc(${asValue(prevUnit)}, ${col}) + floor(${extractedUnit} / ${val}) * interval ${asValue(val + " " + unit)})`;
|
|
772
|
-
// console.log(res);
|
|
773
|
-
return res;
|
|
774
|
-
},
|
|
775
|
-
}) as FunctionSpec,
|
|
776
|
-
),
|
|
777
|
-
|
|
778
|
-
/* Date funcs date_part */
|
|
779
|
-
...["date_trunc", "date_part"].map(
|
|
780
|
-
(funcName) =>
|
|
781
|
-
({
|
|
782
|
-
name: "$" + funcName,
|
|
553
|
+
.map(({ val, unit }) =>
|
|
554
|
+
asFunction({
|
|
555
|
+
name: "$date_trunc_" + (val || "") + unit,
|
|
783
556
|
type: "function",
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
` round down timestamp to closest unit value. `
|
|
789
|
-
: ` extract date unit as float8. `) +
|
|
790
|
-
` E.g. ['hour', col] `,
|
|
791
|
-
singleColArg: false,
|
|
792
|
-
getFields: (args: any[]) => [args[1]],
|
|
557
|
+
description: ` :[column_name, opts?: { timeZone: true | 'TZ Name' }] -> round down timestamp to closest ${val || ""} ${unit} `,
|
|
558
|
+
singleColArg: true,
|
|
559
|
+
numArgs: 2,
|
|
560
|
+
getFields: (args: any[]) => [args[0]],
|
|
793
561
|
getQuery: ({ allColumns, args, tableAliasRaw: tableAlias }) => {
|
|
794
|
-
|
|
562
|
+
/** Timestamp added to ensure filters work correctly (psql will loose the string value timezone when comparing to a non tz column) */
|
|
563
|
+
const col = parseUnix(args[0], tableAlias, allColumns, args[1]);
|
|
564
|
+
if (!val) return `date_trunc(${asValue(unit)}, ${col})`;
|
|
565
|
+
const PreviousUnit = {
|
|
566
|
+
year: "decade",
|
|
567
|
+
month: "year",
|
|
568
|
+
hour: "day",
|
|
569
|
+
minute: "hour",
|
|
570
|
+
second: "minute",
|
|
571
|
+
millisecond: "second",
|
|
572
|
+
microsecond: "millisecond",
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const prevUnit = PreviousUnit[unit as "month"];
|
|
576
|
+
if (!prevUnit) {
|
|
577
|
+
throw "Not supported. prevUnit not found";
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
let extractedUnit = `date_part(${asValue(unit, "::text")}, ${col})::int`;
|
|
581
|
+
if (unit === "microsecond" || unit === "millisecond") {
|
|
582
|
+
extractedUnit = `(${extractedUnit} - 1000 * floor(${extractedUnit}/1000)::int)`;
|
|
583
|
+
}
|
|
584
|
+
const res = `(date_trunc(${asValue(prevUnit)}, ${col}) + floor(${extractedUnit} / ${val}) * interval ${asValue(val + " " + unit)})`;
|
|
585
|
+
// console.log(res);
|
|
586
|
+
return res;
|
|
795
587
|
},
|
|
796
|
-
})
|
|
588
|
+
}),
|
|
589
|
+
),
|
|
590
|
+
|
|
591
|
+
/* Date funcs date_part */
|
|
592
|
+
...["date_trunc", "date_part"].map((funcName) =>
|
|
593
|
+
asFunction({
|
|
594
|
+
name: "$" + funcName,
|
|
595
|
+
type: "function",
|
|
596
|
+
numArgs: 3,
|
|
597
|
+
description:
|
|
598
|
+
` :[unit<string>, column_name, opts?: { timeZone: true | string }] -> ` +
|
|
599
|
+
(funcName === "date_trunc" ?
|
|
600
|
+
` round down timestamp to closest unit value. `
|
|
601
|
+
: ` extract date unit as float8. `) +
|
|
602
|
+
` E.g. ['hour', col] `,
|
|
603
|
+
singleColArg: false,
|
|
604
|
+
getFields: (args: any[]) => [args[1]],
|
|
605
|
+
getQuery: ({ allColumns, args, tableAliasRaw: tableAlias }) => {
|
|
606
|
+
return `${funcName}(${asValue(args[0])}, ${parseUnix(args[1], tableAlias, allColumns, args[2])})`;
|
|
607
|
+
},
|
|
608
|
+
}),
|
|
797
609
|
),
|
|
798
610
|
|
|
799
611
|
/* Handy date funcs */
|
|
@@ -832,23 +644,22 @@ export const FUNCTIONS: FunctionSpec[] = [
|
|
|
832
644
|
["yyyy", "yyyy"],
|
|
833
645
|
["yy", "yy"],
|
|
834
646
|
["yr", "yy"],
|
|
835
|
-
].map(
|
|
836
|
-
(
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
}) as FunctionSpec,
|
|
647
|
+
].map(([funcName, txt]) =>
|
|
648
|
+
asFunction({
|
|
649
|
+
name: "$" + funcName,
|
|
650
|
+
type: "function",
|
|
651
|
+
description:
|
|
652
|
+
` :[column_name, opts?: { timeZone: true | string }] -> get timestamp formated as ` + txt,
|
|
653
|
+
singleColArg: true,
|
|
654
|
+
numArgs: 1,
|
|
655
|
+
getFields: (args: any[]) => [args[0]],
|
|
656
|
+
getQuery: ({ allColumns, args, tableAliasRaw: tableAlias }) => {
|
|
657
|
+
return pgp.as.format(
|
|
658
|
+
"trim(to_char(" + parseUnix(args[0], tableAlias, allColumns, args[1]) + ", $2))",
|
|
659
|
+
[args[0], txt],
|
|
660
|
+
);
|
|
661
|
+
},
|
|
662
|
+
}),
|
|
852
663
|
),
|
|
853
664
|
|
|
854
665
|
/* Basic 1 arg col funcs */
|
|
@@ -865,305 +676,79 @@ export const FUNCTIONS: FunctionSpec[] = [
|
|
|
865
676
|
funcName,
|
|
866
677
|
})),
|
|
867
678
|
),
|
|
868
|
-
].map(
|
|
869
|
-
({
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
}) as FunctionSpec,
|
|
679
|
+
].map(({ funcName, cast }) =>
|
|
680
|
+
asFunction({
|
|
681
|
+
name: "$" + funcName,
|
|
682
|
+
type: "function",
|
|
683
|
+
numArgs: 1,
|
|
684
|
+
singleColArg: true,
|
|
685
|
+
getFields: (args: any[]) => [args[0]],
|
|
686
|
+
getQuery: ({ args, tableAliasRaw: tableAlias }) => {
|
|
687
|
+
return `${funcName}(${asNameAlias(args[0], tableAlias)}${cast ? `::${cast}` : ""})`;
|
|
688
|
+
},
|
|
689
|
+
}),
|
|
880
690
|
),
|
|
881
691
|
|
|
882
692
|
/**
|
|
883
693
|
* Interval funcs
|
|
884
694
|
* (col1, col2?, trunc )
|
|
885
695
|
* */
|
|
886
|
-
...(["age", "ageNow", "difference"] as const).map(
|
|
887
|
-
(
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
if (
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
return trunc ? `date_trunc(${asValue(trunc)}, ${query})` : query;
|
|
920
|
-
},
|
|
921
|
-
}) as FunctionSpec,
|
|
696
|
+
...(["age", "ageNow", "difference"] as const).map((funcName) =>
|
|
697
|
+
asFunction({
|
|
698
|
+
name: "$" + funcName,
|
|
699
|
+
type: "function",
|
|
700
|
+
numArgs: 2,
|
|
701
|
+
singleColArg: true,
|
|
702
|
+
// Filtered because the second arg is optional
|
|
703
|
+
getFields: (args: any[]) => args.slice(0, 2).filter((a) => typeof a === "string"),
|
|
704
|
+
getQuery: ({ args, tableAliasRaw: tableAlias, allColumns }) => {
|
|
705
|
+
const validColCount = args.slice(0, 2).filter((a) => typeof a === "string").length;
|
|
706
|
+
const trunc = args[2];
|
|
707
|
+
const allowedTruncs = ["second", "minute", "hour", "day", "month", "year"];
|
|
708
|
+
if (trunc && !allowedTruncs.includes(trunc))
|
|
709
|
+
throw new Error("Incorrect trunc provided. Allowed values: " + allowedTruncs.join(", "));
|
|
710
|
+
if (funcName === "difference" && validColCount !== 2)
|
|
711
|
+
throw new Error("Must have two column names");
|
|
712
|
+
if (![1, 2].includes(validColCount)) throw new Error("Must have one or two column names");
|
|
713
|
+
const [leftField, rightField] = args as [string, string];
|
|
714
|
+
const tzOpts = args[2];
|
|
715
|
+
const leftQ = parseUnix(leftField, tableAlias, allColumns, tzOpts);
|
|
716
|
+
let rightQ = rightField ? parseUnix(rightField, tableAlias, allColumns, tzOpts) : "";
|
|
717
|
+
let query = "";
|
|
718
|
+
if (funcName === "ageNow" && validColCount === 1) {
|
|
719
|
+
query = `age(now(), ${leftQ})`;
|
|
720
|
+
} else if (funcName === "age" || funcName === "ageNow") {
|
|
721
|
+
if (rightQ) rightQ = ", " + rightQ;
|
|
722
|
+
query = `age(${leftQ} ${rightQ})`;
|
|
723
|
+
} else {
|
|
724
|
+
query = `${leftQ} - ${rightQ}`;
|
|
725
|
+
}
|
|
726
|
+
return trunc ? `date_trunc(${asValue(trunc)}, ${query})` : query;
|
|
727
|
+
},
|
|
728
|
+
}),
|
|
922
729
|
),
|
|
923
730
|
|
|
924
731
|
/* pgcrypto funcs */
|
|
925
|
-
...["crypt"].map(
|
|
926
|
-
(
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
seedColumnName = asNameAlias(args[1], tableAlias);
|
|
936
|
-
|
|
937
|
-
return `crypt(${value}, ${seedColumnName}::text)`;
|
|
938
|
-
},
|
|
939
|
-
}) as FunctionSpec,
|
|
940
|
-
),
|
|
941
|
-
|
|
942
|
-
/* Text col and value funcs */
|
|
943
|
-
...["position", "position_lower"].map(
|
|
944
|
-
(funcName) =>
|
|
945
|
-
({
|
|
946
|
-
name: "$" + funcName,
|
|
947
|
-
type: "function",
|
|
948
|
-
numArgs: 1,
|
|
949
|
-
singleColArg: false,
|
|
950
|
-
getFields: (args: any[]) => [args[1]],
|
|
951
|
-
getQuery: ({ allowedFields, args, tableAliasRaw: tableAlias }) => {
|
|
952
|
-
let a1 = asValue(args[0]),
|
|
953
|
-
a2 = asNameAlias(args[1], tableAlias);
|
|
954
|
-
if (funcName === "position_lower") {
|
|
955
|
-
a1 = `LOWER(${a1}::text)`;
|
|
956
|
-
a2 = `LOWER(${a2}::text)`;
|
|
957
|
-
}
|
|
958
|
-
return `position( ${a1} IN ${a2} )`;
|
|
959
|
-
},
|
|
960
|
-
}) as FunctionSpec,
|
|
961
|
-
),
|
|
962
|
-
...["template_string"].map(
|
|
963
|
-
(funcName) =>
|
|
964
|
-
({
|
|
965
|
-
name: "$" + funcName,
|
|
966
|
-
type: "function",
|
|
967
|
-
numArgs: 1,
|
|
968
|
-
minCols: 0,
|
|
969
|
-
singleColArg: false,
|
|
970
|
-
getFields: (args: any[]) => [] as string[], // Fields not validated because we'll use the allowed ones anyway
|
|
971
|
-
getQuery: ({ allowedFields, args, tableAliasRaw: tableAlias }) => {
|
|
972
|
-
if (typeof args[0] !== "string")
|
|
973
|
-
throw "First argument must be a string. E.g.: '{col1} ..text {col2} ...' ";
|
|
974
|
-
|
|
975
|
-
const rawValue = args[0];
|
|
976
|
-
let finalValue = rawValue;
|
|
977
|
-
const usedColumns = allowedFields.filter((fName) => rawValue.includes(`{${fName}}`));
|
|
978
|
-
usedColumns.forEach((colName, idx) => {
|
|
979
|
-
finalValue = finalValue.split(`{${colName}}`).join(`%${idx + 1}$s`);
|
|
980
|
-
});
|
|
981
|
-
finalValue = asValue(finalValue);
|
|
982
|
-
|
|
983
|
-
if (usedColumns.length) {
|
|
984
|
-
return `format(${finalValue}, ${usedColumns.map((c) => `${asNameAlias(c, tableAlias)}::TEXT`).join(", ")})`;
|
|
985
|
-
}
|
|
732
|
+
...["crypt"].map((funcName) =>
|
|
733
|
+
asFunction({
|
|
734
|
+
name: "$" + funcName,
|
|
735
|
+
type: "function",
|
|
736
|
+
numArgs: 1,
|
|
737
|
+
singleColArg: false,
|
|
738
|
+
getFields: (args: any[]) => [args[1]],
|
|
739
|
+
getQuery: ({ allowedFields, args, tableAliasRaw: tableAlias }) => {
|
|
740
|
+
const value = asValue(args[0]) + "",
|
|
741
|
+
seedColumnName = asNameAlias(args[1], tableAlias);
|
|
986
742
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
743
|
+
return `crypt(${value}, ${seedColumnName}::text)`;
|
|
744
|
+
},
|
|
745
|
+
}),
|
|
990
746
|
),
|
|
991
747
|
|
|
992
|
-
/** Custom highlight -> myterm => ['some text and', ['myterm'], ' and some other text']
|
|
993
|
-
* (fields: "*" | string[], term: string, { edgeTruncate: number = -1; noFields: boolean = false }) => string | (string | [string])[]
|
|
994
|
-
* edgeTruncate = maximum extra characters left and right of matches
|
|
995
|
-
* noFields = exclude field names in search
|
|
996
|
-
* */
|
|
997
|
-
{
|
|
998
|
-
name: "$term_highlight" /* */,
|
|
999
|
-
description: ` :[column_names<string[] | "*">, search_term<string>, opts?<{ returnIndex?: number; edgeTruncate?: number; noFields?: boolean }>] -> get case-insensitive text match highlight`,
|
|
1000
|
-
type: "function",
|
|
1001
|
-
numArgs: 1,
|
|
1002
|
-
singleColArg: true,
|
|
1003
|
-
canBeUsedForFilter: true,
|
|
1004
|
-
getFields: (args: any[]) => args[0],
|
|
1005
|
-
getQuery: ({ allowedFields, args, tableAliasRaw: tableAlias, allColumns }) => {
|
|
1006
|
-
const cols = parseFieldFilter(args[0], false, allowedFields);
|
|
1007
|
-
let term = args[1];
|
|
1008
|
-
const rawTerm = args[1];
|
|
1009
|
-
const { edgeTruncate, noFields = false, returnType, matchCase = false } = args[2] || {};
|
|
1010
|
-
if (!isEmpty(args[2])) {
|
|
1011
|
-
const keys = Object.keys(args[2]);
|
|
1012
|
-
const validKeys = ["edgeTruncate", "noFields", "returnType", "matchCase"];
|
|
1013
|
-
const bad_keys = keys.filter((k) => !validKeys.includes(k));
|
|
1014
|
-
if (bad_keys.length)
|
|
1015
|
-
throw (
|
|
1016
|
-
"Invalid options provided for $term_highlight. Expecting one of: " +
|
|
1017
|
-
validKeys.join(", ")
|
|
1018
|
-
);
|
|
1019
|
-
}
|
|
1020
|
-
if (!cols.length) throw "Cols are empty/invalid";
|
|
1021
|
-
if (typeof term !== "string") throw "Non string term provided: " + term;
|
|
1022
|
-
if (edgeTruncate !== undefined && (!Number.isInteger(edgeTruncate) || edgeTruncate < -1))
|
|
1023
|
-
throw "Invalid edgeTruncate. expecting a positive integer";
|
|
1024
|
-
if (typeof noFields !== "boolean") throw "Invalid noFields. expecting boolean";
|
|
1025
|
-
const RETURN_TYPES = ["index", "boolean", "object"];
|
|
1026
|
-
if (returnType && !RETURN_TYPES.includes(returnType)) {
|
|
1027
|
-
throw `returnType can only be one of: ${RETURN_TYPES}`;
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
const makeTextMatcherArray = (rawText: string, _term: string) => {
|
|
1031
|
-
let matchText = rawText,
|
|
1032
|
-
term = _term;
|
|
1033
|
-
if (!matchCase) {
|
|
1034
|
-
matchText = `LOWER(${rawText})`;
|
|
1035
|
-
term = `LOWER(${term})`;
|
|
1036
|
-
}
|
|
1037
|
-
let leftStr = `substr(${rawText}, 1, position(${term} IN ${matchText}) - 1 )`,
|
|
1038
|
-
rightStr = `substr(${rawText}, position(${term} IN ${matchText}) + length(${term}) )`;
|
|
1039
|
-
if (edgeTruncate) {
|
|
1040
|
-
leftStr = `RIGHT(${leftStr}, ${asValue(edgeTruncate)})`;
|
|
1041
|
-
rightStr = `LEFT(${rightStr}, ${asValue(edgeTruncate)})`;
|
|
1042
|
-
}
|
|
1043
|
-
return `
|
|
1044
|
-
CASE WHEN position(${term} IN ${matchText}) > 0 AND ${term} <> ''
|
|
1045
|
-
THEN array_to_json(ARRAY[
|
|
1046
|
-
to_json( ${leftStr}::TEXT ),
|
|
1047
|
-
array_to_json(
|
|
1048
|
-
ARRAY[substr(${rawText}, position(${term} IN ${matchText}), length(${term}) )::TEXT ]
|
|
1049
|
-
),
|
|
1050
|
-
to_json(${rightStr}::TEXT )
|
|
1051
|
-
])
|
|
1052
|
-
ELSE
|
|
1053
|
-
array_to_json(ARRAY[(${rawText})::TEXT])
|
|
1054
|
-
END
|
|
1055
|
-
`;
|
|
1056
|
-
};
|
|
1057
|
-
|
|
1058
|
-
const colRaw =
|
|
1059
|
-
"( " +
|
|
1060
|
-
cols
|
|
1061
|
-
.map(
|
|
1062
|
-
(c) =>
|
|
1063
|
-
`${noFields ? "" : asValue(c + ": ") + " || "} COALESCE(${asNameAlias(c, tableAlias)}::TEXT, '')`,
|
|
1064
|
-
)
|
|
1065
|
-
.join(" || ', ' || ") +
|
|
1066
|
-
" )";
|
|
1067
|
-
let col = colRaw;
|
|
1068
|
-
term = asValue(term);
|
|
1069
|
-
if (!matchCase) {
|
|
1070
|
-
col = "LOWER" + col;
|
|
1071
|
-
term = `LOWER(${term})`;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
let leftStr = `substr(${colRaw}, 1, position(${term} IN ${col}) - 1 )`,
|
|
1075
|
-
rightStr = `substr(${colRaw}, position(${term} IN ${col}) + length(${term}) )`;
|
|
1076
|
-
if (edgeTruncate) {
|
|
1077
|
-
leftStr = `RIGHT(${leftStr}, ${asValue(edgeTruncate)})`;
|
|
1078
|
-
rightStr = `LEFT(${rightStr}, ${asValue(edgeTruncate)})`;
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
// console.log(col);
|
|
1082
|
-
let res = "";
|
|
1083
|
-
if (returnType === "index") {
|
|
1084
|
-
res = `CASE WHEN position(${term} IN ${col}) > 0 THEN position(${term} IN ${col}) - 1 ELSE -1 END`;
|
|
1085
|
-
|
|
1086
|
-
// } else if(returnType === "boolean"){
|
|
1087
|
-
// res = `CASE WHEN position(${term} IN ${col}) > 0 THEN TRUE ELSE FALSE END`;
|
|
1088
|
-
} else if (returnType === "object" || returnType === "boolean") {
|
|
1089
|
-
const hasChars = Boolean(rawTerm && /[a-z]/i.test(rawTerm));
|
|
1090
|
-
const validCols = cols
|
|
1091
|
-
.map((c) => {
|
|
1092
|
-
const colInfo = allColumns.find((ac) => ac.name === c);
|
|
1093
|
-
return {
|
|
1094
|
-
key: c,
|
|
1095
|
-
colInfo,
|
|
1096
|
-
};
|
|
1097
|
-
})
|
|
1098
|
-
.filter((c) => c.colInfo && c.colInfo.udt_name !== "bytea");
|
|
1099
|
-
|
|
1100
|
-
const _cols = validCols.filter(
|
|
1101
|
-
(c) =>
|
|
1102
|
-
/** Exclude numeric columns when the search tern contains a character */
|
|
1103
|
-
!hasChars || postgresToTsType(c.colInfo!.udt_name) !== "number",
|
|
1104
|
-
);
|
|
1105
|
-
|
|
1106
|
-
/** This will break GROUP BY (non-integer constant in GROUP BY) */
|
|
1107
|
-
if (!_cols.length) {
|
|
1108
|
-
if (validCols.length && hasChars)
|
|
1109
|
-
throw `You're searching the impossible: characters in numeric fields. Use this to prevent making such a request in future: /[a-z]/i.test(your_term) `;
|
|
1110
|
-
return returnType === "boolean" ? "FALSE" : "NULL";
|
|
1111
|
-
}
|
|
1112
|
-
res = `CASE
|
|
1113
|
-
${_cols
|
|
1114
|
-
.map((c) => {
|
|
1115
|
-
const colNameEscaped = asNameAlias(c.key, tableAlias);
|
|
1116
|
-
let colSelect = `${colNameEscaped}::TEXT`;
|
|
1117
|
-
const isTstamp = c.colInfo?.udt_name.startsWith("timestamp");
|
|
1118
|
-
if (isTstamp || c.colInfo?.udt_name === "date") {
|
|
1119
|
-
colSelect = `( CASE WHEN ${colNameEscaped} IS NULL THEN ''
|
|
1120
|
-
ELSE concat_ws(' ',
|
|
1121
|
-
trim(to_char(${colNameEscaped}, 'YYYY-MM-DD HH24:MI:SS')),
|
|
1122
|
-
trim(to_char(${colNameEscaped}, 'Day Month')),
|
|
1123
|
-
'Q' || trim(to_char(${colNameEscaped}, 'Q')),
|
|
1124
|
-
'WK' || trim(to_char(${colNameEscaped}, 'WW'))
|
|
1125
|
-
) END)`;
|
|
1126
|
-
}
|
|
1127
|
-
const colTxt = `COALESCE(${colSelect}, '')`; // position(${term} IN ${colTxt}) > 0
|
|
1128
|
-
if (returnType === "boolean") {
|
|
1129
|
-
return `
|
|
1130
|
-
WHEN ${colTxt} ${matchCase ? "LIKE" : "ILIKE"} ${asValue("%" + rawTerm + "%")}
|
|
1131
|
-
THEN TRUE
|
|
1132
|
-
`;
|
|
1133
|
-
}
|
|
1134
|
-
return `
|
|
1135
|
-
WHEN ${colTxt} ${matchCase ? "LIKE" : "ILIKE"} ${asValue("%" + rawTerm + "%")}
|
|
1136
|
-
THEN json_build_object(
|
|
1137
|
-
${asValue(c.key)},
|
|
1138
|
-
${makeTextMatcherArray(colTxt, term)}
|
|
1139
|
-
)::jsonb
|
|
1140
|
-
`;
|
|
1141
|
-
})
|
|
1142
|
-
.join(" ")}
|
|
1143
|
-
ELSE ${returnType === "boolean" ? "FALSE" : "NULL"}
|
|
1144
|
-
|
|
1145
|
-
END`;
|
|
1146
|
-
|
|
1147
|
-
// console.log(res)
|
|
1148
|
-
} else {
|
|
1149
|
-
/* If no match or empty search THEN return full row as string within first array element */
|
|
1150
|
-
res = `CASE WHEN position(${term} IN ${col}) > 0 AND ${term} <> '' THEN array_to_json(ARRAY[
|
|
1151
|
-
to_json( ${leftStr}::TEXT ),
|
|
1152
|
-
array_to_json(
|
|
1153
|
-
ARRAY[substr(${colRaw}, position(${term} IN ${col}), length(${term}) )::TEXT ]
|
|
1154
|
-
),
|
|
1155
|
-
to_json(${rightStr}::TEXT )
|
|
1156
|
-
]) ELSE array_to_json(ARRAY[(${colRaw})::TEXT]) END`;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
return res;
|
|
1160
|
-
},
|
|
1161
|
-
},
|
|
1162
|
-
|
|
1163
748
|
/* Aggs */
|
|
1164
749
|
...["max", "min", "count", "avg", "json_agg", "jsonb_agg", "string_agg", "array_agg", "sum"].map(
|
|
1165
750
|
(aggName) =>
|
|
1166
|
-
({
|
|
751
|
+
asFunction({
|
|
1167
752
|
name: "$" + aggName,
|
|
1168
753
|
type: "aggregation",
|
|
1169
754
|
numArgs: 1,
|
|
@@ -1176,10 +761,10 @@ export const FUNCTIONS: FunctionSpec[] = [
|
|
|
1176
761
|
}
|
|
1177
762
|
return aggName + "(" + asNameAlias(args[0], tableAlias) + `${extraArgs})`;
|
|
1178
763
|
},
|
|
1179
|
-
})
|
|
764
|
+
}),
|
|
1180
765
|
),
|
|
1181
766
|
|
|
1182
|
-
{
|
|
767
|
+
asFunction({
|
|
1183
768
|
name: "$jsonb_build_object",
|
|
1184
769
|
type: "function",
|
|
1185
770
|
numArgs: 22,
|
|
@@ -1189,10 +774,10 @@ export const FUNCTIONS: FunctionSpec[] = [
|
|
|
1189
774
|
getQuery: ({ args, tableAliasRaw: tableAlias }) => {
|
|
1190
775
|
return `jsonb_build_object(${args.flatMap((arg) => [asValue(arg), asNameAlias(arg, tableAlias)]).join(", ")})`;
|
|
1191
776
|
},
|
|
1192
|
-
},
|
|
777
|
+
}),
|
|
1193
778
|
|
|
1194
779
|
/* More aggs */
|
|
1195
|
-
{
|
|
780
|
+
asFunction({
|
|
1196
781
|
name: "$countAll",
|
|
1197
782
|
type: "aggregation",
|
|
1198
783
|
description: `agg :[] COUNT of all rows `,
|
|
@@ -1202,8 +787,8 @@ export const FUNCTIONS: FunctionSpec[] = [
|
|
|
1202
787
|
getQuery: ({ allowedFields, args, tableAliasRaw: tableAlias }) => {
|
|
1203
788
|
return "COUNT(*)";
|
|
1204
789
|
},
|
|
1205
|
-
}
|
|
1206
|
-
{
|
|
790
|
+
}),
|
|
791
|
+
asFunction({
|
|
1207
792
|
name: "$diff_perc",
|
|
1208
793
|
type: "aggregation",
|
|
1209
794
|
numArgs: 1,
|
|
@@ -1213,7 +798,7 @@ export const FUNCTIONS: FunctionSpec[] = [
|
|
|
1213
798
|
const col = asNameAlias(args[0], tableAlias);
|
|
1214
799
|
return `round( ( ( MAX(${col}) - MIN(${col}) )::float/MIN(${col}) ) * 100, 2)`;
|
|
1215
800
|
},
|
|
1216
|
-
}
|
|
801
|
+
}),
|
|
1217
802
|
];
|
|
1218
803
|
|
|
1219
804
|
/*
|