@adaptabletools/adaptable 24.0.0-canary.9 → 24.0.1

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.
@@ -9,6 +9,13 @@ import { badgePredicateToBooleanExpression, columnFilterPredicatesToExpression,
9
9
  import { migrateGreenfieldExpressionTexts } from './migrateGreenfieldExpressionTexts';
10
10
  import { isAdaptableSharedEntity } from '../AdaptableState/TeamSharingState';
11
11
  import { migrateLayoutTagsToRegistry } from './migrateLayoutTagsToRegistry';
12
+ const UNCONVERTED_PREDICATE_OUTCOME = {
13
+ 'Column Filter': 'they keep their legacy Predicates, which are ignored, so the filter is inactive',
14
+ Alert: 'their Rule becomes empty, so the Alert never fires',
15
+ 'Flashing Cell': 'their Rule becomes empty, so the Flashing Cell never flashes',
16
+ 'Format Column': 'their Rule is removed, so the format applies to every cell in Scope',
17
+ 'Badge Style': 'the Badge has no Rule, so it is shown as the fallback (always) Badge',
18
+ };
12
19
  const NEEDS_MIGRATION_SUFFIX = ' (needs migration)';
13
20
  const LEGACY_SOURCE_PATTERN = new RegExp(`\\b(${[...LEGACY_OBSERVABLE_SOURCE_NAMES].join('|')})\\s*[({]`);
14
21
  export class VersionUpgrade24 extends VersionUpgrade {
@@ -17,13 +24,19 @@ export class VersionUpgrade24 extends VersionUpgrade {
17
24
  super(logger);
18
25
  this.options = options;
19
26
  }
27
+ customFunctionUsages = new Map();
28
+ percentChangeObjects = [];
20
29
  migrateState(state) {
30
+ this.customFunctionUsages.clear();
31
+ this.percentChangeObjects = [];
21
32
  this.migrateNoteTimestamps(state);
22
33
  this.migrateColumnFilters(state);
23
34
  this.migrateAlertRules(state);
24
35
  this.migrateFlashingCellRules(state);
25
36
  this.migrateFormatColumnRules(state);
26
37
  this.migrateBadgeStyleRules(state);
38
+ this.logCustomPredicateFunctions();
39
+ this.logPercentChangeApproximations();
27
40
  this.migrateExpressionQueryKeys(state);
28
41
  this.migrateNamedQueries(state);
29
42
  this.migrateCalculatedColumnTypes(state);
@@ -92,13 +105,14 @@ export class VersionUpgrade24 extends VersionUpgrade {
92
105
  return;
93
106
  }
94
107
  let migratedCount = 0;
95
- let preservedCount = 0;
108
+ const preserved = [];
96
109
  for (const layout of layouts) {
97
110
  const filters = layout.ColumnFilters;
98
111
  if (!filters?.length) {
99
112
  continue;
100
113
  }
101
114
  for (const filter of filters) {
115
+ const label = `Column Filter '${filter.ColumnId}' (Layout '${layout.Name}')`;
102
116
  if (filter.Expression) {
103
117
  delete filter.Predicates;
104
118
  delete filter.PredicatesOperator;
@@ -108,12 +122,15 @@ export class VersionUpgrade24 extends VersionUpgrade {
108
122
  if (!filter.Predicates?.length) {
109
123
  continue;
110
124
  }
111
- const expression = columnFilterPredicatesToExpression(filter);
125
+ const customCalls = [];
126
+ const expression = columnFilterPredicatesToExpression(filter, customCalls);
112
127
  if (expression != null) {
113
128
  filter.Expression = expression;
129
+ this.recordPercentChange(filter.Predicates, label);
114
130
  delete filter.PreferredFilter;
115
131
  delete filter.Predicates;
116
132
  delete filter.PredicatesOperator;
133
+ this.recordCustomFunctionCalls(customCalls, label);
117
134
  migratedCount++;
118
135
  continue;
119
136
  }
@@ -126,10 +143,10 @@ export class VersionUpgrade24 extends VersionUpgrade {
126
143
  migratedCount++;
127
144
  continue;
128
145
  }
129
- preservedCount++;
146
+ preserved.push(this.describePredicates(label, filter.Predicates));
130
147
  }
131
148
  }
132
- this.logPredicateMigrationSummary('Column Filter', migratedCount, preservedCount);
149
+ this.logPredicateMigrationSummary('Column Filter', migratedCount, preserved);
133
150
  }
134
151
  migrateAlertRules(state) {
135
152
  this.migratePredicateRulesInDefinitions(state.Alert?.AlertDefinitions, 'Alert', (definition) => definition.Rule);
@@ -146,51 +163,58 @@ export class VersionUpgrade24 extends VersionUpgrade {
146
163
  return;
147
164
  }
148
165
  let migratedCount = 0;
149
- let preservedCount = 0;
166
+ const preserved = [];
150
167
  for (const styledColumn of columns) {
151
168
  const badges = styledColumn.BadgeStyle?.Badges;
152
169
  if (!badges?.length) {
153
170
  continue;
154
171
  }
155
- for (const badge of badges) {
172
+ badges.forEach((badge, index) => {
156
173
  const legacyExpression = badge.Expression;
157
174
  if (badge.Rule?.Expression || legacyExpression?.Expression || legacyExpression?.BooleanExpression) {
158
175
  delete badge.Predicate;
159
- continue;
176
+ return;
160
177
  }
161
178
  if (!badge.Predicate?.PredicateId) {
162
- continue;
179
+ return;
163
180
  }
164
- const expression = badgePredicateToBooleanExpression(badge.Predicate, styledColumn.ColumnId);
181
+ const label = `Badge ${index + 1} of Styled Column '${styledColumn.Name ?? styledColumn.ColumnId}'`;
182
+ const customCalls = [];
183
+ const expression = badgePredicateToBooleanExpression(badge.Predicate, styledColumn.ColumnId, customCalls);
165
184
  if (expression == null) {
166
- preservedCount++;
167
- continue;
185
+ preserved.push(this.describePredicates(label, [badge.Predicate]));
186
+ return;
168
187
  }
169
188
  badge.Rule = { Expression: expression };
189
+ this.recordPercentChange([badge.Predicate], label);
170
190
  delete badge.Predicate;
191
+ this.recordCustomFunctionCalls(customCalls, label);
171
192
  migratedCount++;
172
- }
193
+ });
173
194
  }
174
- this.logPredicateMigrationSummary('Badge Style', migratedCount, preservedCount);
195
+ this.logPredicateMigrationSummary('Badge Style', migratedCount, preserved);
175
196
  }
176
197
  migratePredicateRulesInDefinitions(definitions, moduleLabel, getRule) {
177
198
  if (!definitions?.length) {
178
199
  return;
179
200
  }
180
201
  let migratedCount = 0;
181
- let preservedCount = 0;
182
- for (const definition of definitions) {
183
- const result = this.migratePredicateRule(getRule(definition));
202
+ const preserved = [];
203
+ definitions.forEach((definition, index) => {
204
+ const name = definition.Name;
205
+ const label = `${moduleLabel} '${typeof name === 'string' && name ? name : `#${index + 1}`}'`;
206
+ const rule = getRule(definition);
207
+ const result = this.migratePredicateRule(rule, label);
184
208
  if (result === 'migrated') {
185
209
  migratedCount++;
186
210
  }
187
211
  if (result === 'preserved') {
188
- preservedCount++;
212
+ preserved.push(this.describePredicates(label, rule?.Predicates));
189
213
  }
190
- }
191
- this.logPredicateMigrationSummary(moduleLabel, migratedCount, preservedCount);
214
+ });
215
+ this.logPredicateMigrationSummary(moduleLabel, migratedCount, preserved);
192
216
  }
193
- migratePredicateRule(rule) {
217
+ migratePredicateRule(rule, label) {
194
218
  if (!isLegacyAdaptableQuery(rule)) {
195
219
  return 'skip';
196
220
  }
@@ -209,12 +233,15 @@ export class VersionUpgrade24 extends VersionUpgrade {
209
233
  if (!rule.Predicates?.length) {
210
234
  return 'skip';
211
235
  }
212
- const expression = rulePredicatesToBooleanExpression(rule.Predicates);
236
+ const customCalls = [];
237
+ const expression = rulePredicatesToBooleanExpression(rule.Predicates, customCalls);
213
238
  if (expression == null) {
214
239
  return 'preserved';
215
240
  }
241
+ this.recordPercentChange(rule.Predicates, label);
216
242
  delete rule.Predicates;
217
243
  rule.Expression = expression;
244
+ this.recordCustomFunctionCalls(customCalls, label);
218
245
  return 'migrated';
219
246
  }
220
247
  migrateExpressionQueryKeys(state) {
@@ -397,12 +424,57 @@ export class VersionUpgrade24 extends VersionUpgrade {
397
424
  if (migrated > 0) {
398
425
  this.logger.info(`Migrated ${migrated} ${module} predicate rule(s) to AdaptableQL Expression (v24).`);
399
426
  }
400
- if (preserved > 0) {
401
- this.logger.warn(`Left ${preserved} ${module} predicate rule(s) unchanged (no lossless AdaptableQL ` +
402
- `equivalent — e.g. a custom predicate or PercentChange); they continue to ` +
403
- `evaluate via the legacy predicate path.`);
427
+ if (preserved.length > 0) {
428
+ this.logger.warn(`Could not convert ${preserved.length} ${module} predicate rule(s) to an AdaptableQL ` +
429
+ `Expression (v24): ${preserved.join('; ')}. Predicates are not evaluated in v24: ` +
430
+ `${UNCONVERTED_PREDICATE_OUTCOME[module]}. Re-author them as Expressions.`);
431
+ }
432
+ }
433
+ describePredicates(label, predicates) {
434
+ const ids = (predicates ?? []).map((predicate) => predicate?.PredicateId ?? '?');
435
+ return ids.length ? `${label} (${ids.join(', ')})` : label;
436
+ }
437
+ recordCustomFunctionCalls(calls, label) {
438
+ for (const call of calls) {
439
+ let usage = this.customFunctionUsages.get(call.functionName);
440
+ if (!usage) {
441
+ usage = { predicateIds: new Set(), maxInputCount: 0, objects: [] };
442
+ this.customFunctionUsages.set(call.functionName, usage);
443
+ }
444
+ usage.predicateIds.add(call.predicateId);
445
+ usage.maxInputCount = Math.max(usage.maxInputCount, call.inputCount);
446
+ if (!usage.objects.includes(label)) {
447
+ usage.objects.push(label);
448
+ }
449
+ }
450
+ }
451
+ recordPercentChange(predicates, label) {
452
+ if (predicates?.some((predicate) => predicate?.PredicateId === 'PercentChange') &&
453
+ !this.percentChangeObjects.includes(label)) {
454
+ this.percentChangeObjects.push(label);
404
455
  }
405
456
  }
457
+ logPercentChangeApproximations() {
458
+ if (!this.percentChangeObjects.length) {
459
+ return;
460
+ }
461
+ this.logger.warn(`PercentChange Predicate migrated to PERCENT_CHANGE(...) > n in: ${this.percentChangeObjects.join(', ')} (v24). ` +
462
+ `PERCENT_CHANGE divides the change by the previous value; PercentChange divided it by the smaller ` +
463
+ `of the previous and new values. Rises give the same percentage, but falls give a smaller one ` +
464
+ `(a fall from 100 to 94 was 6.38% and is now 6%), so these rules can fire less often on falls; ` +
465
+ `lower the threshold if needed.`);
466
+ }
467
+ logCustomPredicateFunctions() {
468
+ this.customFunctionUsages.forEach((usage, functionName) => {
469
+ const ids = [...usage.predicateIds].map((id) => `'${id}'`).join(', ');
470
+ const args = usage.maxInputCount > 0
471
+ ? `args[0] is the Column value, args[1..${usage.maxInputCount}] are the former Predicate Inputs`
472
+ : 'args[0] is the Column value';
473
+ this.logger.info(`Custom Predicate ${ids} migrated to ${functionName}(...) in: ${usage.objects.join(', ')} (v24). ` +
474
+ `Define ${functionName} in expressionOptions.customStandardFunctions with returnType ` +
475
+ `'boolean'; ${args}. Until it is defined these rules are invalid and are not applied.`);
476
+ });
477
+ }
406
478
  migrateQueryWrappersToStrings(state) {
407
479
  let migratedCount = 0;
408
480
  const textOf = (value) => {
@@ -13,7 +13,7 @@ export type LegacyPredicate = {
13
13
  /**
14
14
  * Maps a legacy predicate id + raw inputs to the equivalent AQL
15
15
  * `QlPredicate` (primitive or AND/OR group). Returns `null` for custom
16
- * defs and `PercentChange`.
16
+ * defs (lowered to custom function calls by `predicateExpressionMigration`).
17
17
  */
18
18
  export declare const predicateIdToQlPredicate: (predicateId: string, inputs: unknown[] | undefined, column: LegacyPredicateColumn) => QlPredicate | null;
19
19
  /**
@@ -134,6 +134,8 @@ export const predicateIdToQlPredicate = (predicateId, inputs, column) => {
134
134
  }
135
135
  case 'AnyChange':
136
136
  return qlPrimitive('ANY_CHANGE', [col]);
137
+ case 'PercentChange':
138
+ return qlPrimitive('GT', [qlPrimitive('PERCENT_CHANGE', [col]), ...literals.slice(0, 1)]);
137
139
  default:
138
140
  return null;
139
141
  }
@@ -16,14 +16,23 @@
16
16
  * (they evaluate against the scoped column at runtime, exactly what
17
17
  * `$SCOPE` resolves to).
18
18
  *
19
+ * Custom predicate ids (user `customPredicateDefs`) lower to a call to the
20
+ * custom function that replaces them in v24: the id in UPPER_SNAKE form,
21
+ * the subject first and the stored `Inputs` after it
22
+ * (`largeNotional` → `LARGE_NOTIONAL([notional])`). The function itself is
23
+ * not checked — it must be defined in `customStandardFunctions` at runtime,
24
+ * otherwise the rule is invalid (logged, never applied).
25
+ *
19
26
  * Every helper is **all-or-nothing**: if any predicate in the set cannot
20
- * be lowered to a complete AQL primitive (custom predicate defs,
21
- * `PercentChange` — AQL `PERCENT_CHANGE` is number-returning — or an
22
- * incomplete/empty predicate) the helper returns `null` and the caller
23
- * must leave the legacy predicate representation intact. This preserves
24
- * the runtime fallback and keeps the migration idempotent.
27
+ * be lowered to a complete AQL primitive (a custom id with no valid or
28
+ * non-clashing function name, or an incomplete/empty predicate) the helper
29
+ * returns `null` and the caller must leave the legacy predicate
30
+ * representation intact. This keeps the migration idempotent.
25
31
  *
26
- * `AnyChange` lowers to `ANY_CHANGE([col])` / `ANY_CHANGE($SCOPE)`.
32
+ * `AnyChange` lowers to `ANY_CHANGE([col])` / `ANY_CHANGE($SCOPE)`;
33
+ * `PercentChange` to `PERCENT_CHANGE([col]) > n`, which divides the change
34
+ * by the previous value where the predicate divided it by the smaller of
35
+ * the previous and new values.
27
36
  */
28
37
  import { ColumnFilter } from '../AdaptableState/Common/ColumnFilter';
29
38
  import { LegacyPredicate } from './legacyPredicateLowering';
@@ -32,15 +41,37 @@ import { QlPredicate, QlPrimitive } from "../../vendor/aql/index.js";
32
41
  type LegacyColumnPredicate = LegacyPredicate & {
33
42
  ColumnId?: string;
34
43
  };
44
+ /**
45
+ * A custom predicate that was lowered to a call to the custom function
46
+ * replacing it.
47
+ */
48
+ export interface CustomPredicateFunctionCall {
49
+ predicateId: string;
50
+ functionName: string;
51
+ /** Number of stored `Inputs`, passed after the subject (`args[1..]`). */
52
+ inputCount: number;
53
+ }
54
+ /**
55
+ * The custom function name a custom predicate id migrates to: the id in
56
+ * UPPER_SNAKE form (`largeNotional` → `LARGE_NOTIONAL`, `isUSDTrade` →
57
+ * `IS_USD_TRADE`, `top-tier` → `TOP_TIER`).
58
+ *
59
+ * @returns the name, or `null` when the id is a v23 system predicate, the
60
+ * name is not a valid AQL function name, or it clashes with a system
61
+ * function (the rule would silently call the system function).
62
+ */
63
+ export declare const customPredicateIdToFunctionName: (predicateId: string) => string | null;
35
64
  /**
36
65
  * Lowers a single legacy predicate to a complete AQL `QlPredicate`.
37
66
  *
38
67
  * @param subjectColumnId the column the predicate applies to, or `null`
39
68
  * to emit a `$SCOPE` subject (rule predicate without a `ColumnId`).
40
- * @returns the predicate, or `null` when the predicate has no lossless
41
- * AQL equivalent or is incomplete.
69
+ * @param customCalls receives the custom function call when the predicate
70
+ * is a custom predicate.
71
+ * @returns the predicate, or `null` when the predicate has no AQL
72
+ * equivalent or is incomplete.
42
73
  */
43
- export declare const lowerPredicateToQlPredicate: (predicate: LegacyPredicate, subjectColumnId: string | null) => QlPredicate | null;
74
+ export declare const lowerPredicateToQlPredicate: (predicate: LegacyPredicate, subjectColumnId: string | null, customCalls?: CustomPredicateFunctionCall[]) => QlPredicate | null;
44
75
  /** @deprecated Use {@link lowerPredicateToQlPredicate} — range predicates lower to groups. */
45
76
  export declare const lowerPredicateToPrimitive: (predicate: LegacyPredicate, subjectColumnId: string | null) => QlPrimitive | null;
46
77
  /**
@@ -50,8 +81,10 @@ export declare const lowerPredicateToPrimitive: (predicate: LegacyPredicate, sub
50
81
  *
51
82
  * Returns `null` when the filter has no predicates or any predicate is
52
83
  * unconvertible — the caller keeps the legacy `Predicates`.
84
+ *
85
+ * @param customCalls receives the custom function calls of a converted filter.
53
86
  */
54
- export declare const columnFilterPredicatesToExpression: (columnFilter: ColumnFilter) => string | null;
87
+ export declare const columnFilterPredicatesToExpression: (columnFilter: ColumnFilter, customCalls?: CustomPredicateFunctionCall[]) => string | null;
55
88
  /**
56
89
  * Converts a single incomplete ColumnFilter Predicate (empty Inputs —
57
90
  * operator seed only) to a `PreferredFilter` AQL operator name.
@@ -67,11 +100,15 @@ export declare const columnFilterPredicatesToPreferredFilter: (columnFilter: Col
67
100
  * predicate uses its own `ColumnId`, or `$SCOPE` when absent.
68
101
  *
69
102
  * Returns `null` when there are no predicates or any is unconvertible.
103
+ *
104
+ * @param customCalls receives the custom function calls of a converted rule.
70
105
  */
71
- export declare const rulePredicatesToBooleanExpression: (predicates: LegacyColumnPredicate[] | undefined) => string | null;
106
+ export declare const rulePredicatesToBooleanExpression: (predicates: LegacyColumnPredicate[] | undefined, customCalls?: CustomPredicateFunctionCall[]) => string | null;
72
107
  /**
73
108
  * Lowers a single Badge Style `Predicate` (subject = the styled column)
74
109
  * to a `BooleanExpression`. Returns `null` when unconvertible.
110
+ *
111
+ * @param customCalls receives the custom function call of a converted badge.
75
112
  */
76
- export declare const badgePredicateToBooleanExpression: (predicate: LegacyPredicate, columnId: string) => string | null;
113
+ export declare const badgePredicateToBooleanExpression: (predicate: LegacyPredicate, columnId: string, customCalls?: CustomPredicateFunctionCall[]) => string | null;
77
114
  export {};
@@ -1,6 +1,6 @@
1
- import { systemScalarExpressionFunctions as scalarExpressionFunctions } from '../aql/systemExpressionFunctions';
1
+ import { systemAggregatedExpressionFunctions, systemAggregationClauseFunctions, systemObservableExpressionFunctions, systemScalarExpressionFunctions as scalarExpressionFunctions, } from '../aql/systemExpressionFunctions';
2
2
  import { predicateIdToQlPredicate, } from './legacyPredicateLowering';
3
- import { isQlColRef, isQlPredicateComplete, mapQlPredicateToExpression, migrateExpression, } from "../../vendor/aql/index.js";
3
+ import { AGGREGATION_ONLY_NAMES, colRef, DUAL_AGGREGATION_NAMES, isAST_Function, isQlColRef, isQlPredicateComplete, LEGACY_OBSERVABLE_MARKER_NAMES, LEGACY_OBSERVABLE_SOURCE_NAMES, literalForRawValue, mapQlPredicateToExpression, migrateExpression, OBSERVABLE_SOURCE_NAMES, OPERAND_CLAUSE_NAMES, parse, qlPrimitive, } from "../../vendor/aql/index.js";
4
4
  const MIGRATION_FUNCTION_POOL = {
5
5
  scalarFunctions: scalarExpressionFunctions,
6
6
  };
@@ -33,12 +33,14 @@ const NUMBER_PREDICATE_IDS = new Set([
33
33
  'Positive',
34
34
  'Negative',
35
35
  'Zero',
36
+ 'PercentChange',
36
37
  ]);
37
38
  const NUMERIC_INPUT_PREDICATE_IDS = new Set([
38
39
  'GreaterThan',
39
40
  'LessThan',
40
41
  'Between',
41
42
  'NotBetween',
43
+ 'PercentChange',
42
44
  ]);
43
45
  const BOOLEAN_PREDICATE_IDS = new Set(['True', 'False', 'BooleanToggle']);
44
46
  const SCOPE_REF = { kind: 'scope' };
@@ -92,14 +94,139 @@ const withScopeSubject = (predicate) => {
92
94
  }),
93
95
  };
94
96
  };
95
- export const lowerPredicateToQlPredicate = (predicate, subjectColumnId) => {
97
+ const SYSTEM_PREDICATE_IDS = new Set([
98
+ 'After',
99
+ 'AnyChange',
100
+ 'Before',
101
+ 'Between',
102
+ 'Blanks',
103
+ 'BooleanToggle',
104
+ 'Contains',
105
+ 'EndsWith',
106
+ 'Equals',
107
+ 'False',
108
+ 'GreaterThan',
109
+ 'GreaterThanOrEqual',
110
+ 'Holiday',
111
+ 'In',
112
+ 'InFuture',
113
+ 'InPast',
114
+ 'InRange',
115
+ 'Is',
116
+ 'IsNot',
117
+ 'LastWorkDay',
118
+ 'LessThan',
119
+ 'LessThanOrEqual',
120
+ 'Negative',
121
+ 'NextWorkDay',
122
+ 'NonBlanks',
123
+ 'NotBetween',
124
+ 'NotContains',
125
+ 'NotEquals',
126
+ 'NotIn',
127
+ 'NotOn',
128
+ 'On',
129
+ 'OnOrAfter',
130
+ 'OnOrBefore',
131
+ 'PercentChange',
132
+ 'Positive',
133
+ 'Range',
134
+ 'Regex',
135
+ 'StartsWith',
136
+ 'ThisMonth',
137
+ 'ThisQuarter',
138
+ 'ThisWeek',
139
+ 'ThisYear',
140
+ 'Today',
141
+ 'Tomorrow',
142
+ 'True',
143
+ 'WorkDay',
144
+ 'Yesterday',
145
+ 'Zero',
146
+ ]);
147
+ let systemFunctionNames;
148
+ const getSystemFunctionNames = () => {
149
+ if (!systemFunctionNames) {
150
+ systemFunctionNames = new Set([
151
+ ...Object.keys(scalarExpressionFunctions),
152
+ ...Object.keys(systemAggregatedExpressionFunctions),
153
+ ...Object.keys(systemAggregationClauseFunctions),
154
+ ...Object.keys(systemObservableExpressionFunctions),
155
+ ...AGGREGATION_ONLY_NAMES,
156
+ ...DUAL_AGGREGATION_NAMES,
157
+ ...OBSERVABLE_SOURCE_NAMES,
158
+ ...LEGACY_OBSERVABLE_SOURCE_NAMES,
159
+ ...LEGACY_OBSERVABLE_MARKER_NAMES,
160
+ ...OPERAND_CLAUSE_NAMES,
161
+ ]);
162
+ }
163
+ return systemFunctionNames;
164
+ };
165
+ const FUNCTION_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/;
166
+ const parsesAsFunctionCall = (name) => {
167
+ try {
168
+ const { ast } = parse(`${name}(1)`);
169
+ return ast.length === 1 && isAST_Function(ast[0]) && ast[0].type === name;
170
+ }
171
+ catch {
172
+ return false;
173
+ }
174
+ };
175
+ export const customPredicateIdToFunctionName = (predicateId) => {
176
+ if (!predicateId || SYSTEM_PREDICATE_IDS.has(predicateId)) {
177
+ return null;
178
+ }
179
+ const name = predicateId
180
+ .trim()
181
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
182
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
183
+ .replace(/[^A-Za-z0-9]+/g, '_')
184
+ .replace(/^_+|_+$/g, '')
185
+ .toUpperCase();
186
+ if (!FUNCTION_NAME_PATTERN.test(name) || getSystemFunctionNames().has(name)) {
187
+ return null;
188
+ }
189
+ return parsesAsFunctionCall(name) ? name : null;
190
+ };
191
+ const lowerCustomPredicate = (predicate, subjectColumnId) => {
192
+ const functionName = customPredicateIdToFunctionName(predicate.PredicateId);
193
+ if (!functionName) {
194
+ return null;
195
+ }
196
+ const inputs = (predicate.Inputs ?? []).filter((input) => input !== undefined);
197
+ const isScalarInput = (input) => typeof input === 'string' || typeof input === 'number' || typeof input === 'boolean';
198
+ if (!inputs.every(isScalarInput)) {
199
+ return null;
200
+ }
201
+ const subject = subjectColumnId == null ? SCOPE_REF : colRef(subjectColumnId);
202
+ const qlPredicate = qlPrimitive(functionName, [
203
+ subject,
204
+ ...inputs.map((input) => literalForRawValue(input)),
205
+ ]);
206
+ if (!isQlPredicateComplete(qlPredicate, MIGRATION_FUNCTION_POOL)) {
207
+ return null;
208
+ }
209
+ return {
210
+ qlPredicate,
211
+ call: { predicateId: predicate.PredicateId, functionName, inputCount: inputs.length },
212
+ };
213
+ };
214
+ export const lowerPredicateToQlPredicate = (predicate, subjectColumnId, customCalls) => {
96
215
  const predicateId = predicate.PredicateId;
97
216
  const column = {
98
217
  columnId: subjectColumnId ?? '__scope__',
99
218
  dataType: inferDataType(predicateId, predicate.Inputs),
100
219
  };
101
220
  const qlPredicate = predicateIdToQlPredicate(predicateId, coerceInputs(predicateId, predicate.Inputs), column);
102
- if (!qlPredicate || !isQlPredicateComplete(qlPredicate, MIGRATION_FUNCTION_POOL)) {
221
+ if (!qlPredicate) {
222
+ const custom = lowerCustomPredicate(predicate, subjectColumnId);
223
+ if (!custom) {
224
+ return null;
225
+ }
226
+ customCalls?.push(custom.call);
227
+ return custom.qlPredicate;
228
+ }
229
+ if (!isQlPredicateComplete(qlPredicate, MIGRATION_FUNCTION_POOL)) {
103
230
  return null;
104
231
  }
105
232
  return subjectColumnId == null ? withScopeSubject(qlPredicate) : qlPredicate;
@@ -119,20 +246,22 @@ const toGroupExpression = (children, combinator) => {
119
246
  const group = { kind: 'group', combinator, children };
120
247
  return toCanonicalExpression(mapQlPredicateToExpression(group));
121
248
  };
122
- export const columnFilterPredicatesToExpression = (columnFilter) => {
249
+ export const columnFilterPredicatesToExpression = (columnFilter, customCalls) => {
123
250
  const legacy = columnFilter;
124
251
  const predicates = legacy.Predicates ?? [];
125
252
  if (!predicates.length) {
126
253
  return null;
127
254
  }
255
+ const calls = [];
128
256
  const children = [];
129
257
  for (const predicate of predicates) {
130
- const qlPredicate = lowerPredicateToQlPredicate(predicate, columnFilter.ColumnId);
258
+ const qlPredicate = lowerPredicateToQlPredicate(predicate, columnFilter.ColumnId, calls);
131
259
  if (!qlPredicate) {
132
260
  return null;
133
261
  }
134
262
  children.push(qlPredicate);
135
263
  }
264
+ customCalls?.push(...calls);
136
265
  const combinator = legacy.PredicatesOperator === 'OR' ? 'OR' : 'AND';
137
266
  return toGroupExpression(children, combinator);
138
267
  };
@@ -178,22 +307,24 @@ export const columnFilterPredicatesToPreferredFilter = (columnFilter) => {
178
307
  }
179
308
  return operator;
180
309
  };
181
- export const rulePredicatesToBooleanExpression = (predicates) => {
310
+ export const rulePredicatesToBooleanExpression = (predicates, customCalls) => {
182
311
  if (!predicates?.length) {
183
312
  return null;
184
313
  }
314
+ const calls = [];
185
315
  const children = [];
186
316
  for (const predicate of predicates) {
187
- const qlPredicate = lowerPredicateToQlPredicate(predicate, predicate.ColumnId ?? null);
317
+ const qlPredicate = lowerPredicateToQlPredicate(predicate, predicate.ColumnId ?? null, calls);
188
318
  if (!qlPredicate) {
189
319
  return null;
190
320
  }
191
321
  children.push(qlPredicate);
192
322
  }
323
+ customCalls?.push(...calls);
193
324
  return toGroupExpression(children, 'AND');
194
325
  };
195
- export const badgePredicateToBooleanExpression = (predicate, columnId) => {
196
- const qlPredicate = lowerPredicateToQlPredicate(predicate, columnId);
326
+ export const badgePredicateToBooleanExpression = (predicate, columnId, customCalls) => {
327
+ const qlPredicate = lowerPredicateToQlPredicate(predicate, columnId, customCalls);
197
328
  if (!qlPredicate) {
198
329
  return null;
199
330
  }