@milaboratories/pl-model-common 1.10.6 → 1.11.0

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.
Files changed (46) hide show
  1. package/dist/drivers/pframe/column_filter.d.ts +1 -1
  2. package/dist/drivers/pframe/column_filter.d.ts.map +1 -1
  3. package/dist/drivers/pframe/data.d.ts +1 -1
  4. package/dist/drivers/pframe/data.d.ts.map +1 -1
  5. package/dist/drivers/pframe/driver.d.ts +1 -1
  6. package/dist/drivers/pframe/driver.d.ts.map +1 -1
  7. package/dist/drivers/pframe/find_columns.d.ts +1 -1
  8. package/dist/drivers/pframe/find_columns.d.ts.map +1 -1
  9. package/dist/drivers/pframe/index.d.ts +2 -1
  10. package/dist/drivers/pframe/index.d.ts.map +1 -1
  11. package/dist/drivers/pframe/pframe.d.ts +1 -1
  12. package/dist/drivers/pframe/pframe.d.ts.map +1 -1
  13. package/dist/drivers/pframe/spec/anchored_id.d.ts +47 -0
  14. package/dist/drivers/pframe/spec/anchored_id.d.ts.map +1 -0
  15. package/dist/drivers/pframe/spec/index.d.ts +4 -0
  16. package/dist/drivers/pframe/spec/index.d.ts.map +1 -0
  17. package/dist/drivers/pframe/spec/selectors.d.ts +165 -0
  18. package/dist/drivers/pframe/spec/selectors.d.ts.map +1 -0
  19. package/dist/drivers/pframe/{spec.d.ts → spec/spec.d.ts} +1 -1
  20. package/dist/drivers/pframe/spec/spec.d.ts.map +1 -0
  21. package/dist/drivers/pframe/table_common.d.ts +1 -1
  22. package/dist/drivers/pframe/table_common.d.ts.map +1 -1
  23. package/dist/drivers/pframe/unique_values.d.ts +1 -1
  24. package/dist/drivers/pframe/unique_values.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +472 -245
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/ref.d.ts +7 -0
  30. package/dist/ref.d.ts.map +1 -1
  31. package/package.json +8 -2
  32. package/src/drivers/pframe/column_filter.ts +1 -1
  33. package/src/drivers/pframe/data.ts +1 -1
  34. package/src/drivers/pframe/driver.ts +1 -1
  35. package/src/drivers/pframe/find_columns.ts +1 -1
  36. package/src/drivers/pframe/index.ts +3 -1
  37. package/src/drivers/pframe/pframe.ts +1 -1
  38. package/src/drivers/pframe/spec/anchored_id.ts +215 -0
  39. package/src/drivers/pframe/spec/index.ts +3 -0
  40. package/src/drivers/pframe/spec/selectors.test.ts +209 -0
  41. package/src/drivers/pframe/spec/selectors.ts +267 -0
  42. package/src/drivers/pframe/{spec.ts → spec/spec.ts} +3 -3
  43. package/src/drivers/pframe/table_common.ts +1 -1
  44. package/src/drivers/pframe/unique_values.ts +1 -1
  45. package/src/ref.ts +17 -0
  46. package/dist/drivers/pframe/spec.d.ts.map +0 -1
@@ -0,0 +1,215 @@
1
+ import canonicalize from 'canonicalize';
2
+ import type { AxisId, PColumnSpec } from './spec';
3
+ import { getAxisId, matchAxisId } from './spec';
4
+ import type { AAxisSelector, AnchorAxisRef, AnchorAxisRefByIdx, APColumnId, APColumnSelector, AxisSelector, PColumnSelector } from './selectors';
5
+
6
+ //
7
+ // Helper functions
8
+ //
9
+
10
+ function axisKey(axis: AxisId): string {
11
+ return canonicalize(getAxisId(axis))!;
12
+ }
13
+
14
+ function domainKey(key: string, value: string): string {
15
+ return JSON.stringify([key, value]);
16
+ }
17
+
18
+ /**
19
+ * Context for resolving and generating anchored references to columns and axes
20
+ * Maintains maps of known domain values and axes that can be referenced by anchors
21
+ */
22
+ export class AnchorIdDeriver {
23
+ private readonly domains = new Map<string, string>();
24
+ private readonly axes = new Map<string, AnchorAxisRefByIdx>();
25
+ /**
26
+ * Domain packs are used to group domain keys that can be anchored to the same anchor
27
+ * This is used to optimize the lookup of domain anchors
28
+ */
29
+ private readonly domainPacks: string[][] = [];
30
+ /**
31
+ * Maps domain packs to anchors
32
+ */
33
+ private readonly domainPackToAnchor = new Map<string, string>();
34
+
35
+ /**
36
+ * Creates a new anchor context from a set of anchor column specifications
37
+ * @param anchors Record of anchor column specifications indexed by anchor ID
38
+ */
39
+ constructor(private readonly anchors: Record<string, PColumnSpec>) {
40
+ const anchorEntries = Object.entries(anchors);
41
+ anchorEntries.sort((a, b) => a[0].localeCompare(b[0]));
42
+ for (const [anchorId, spec] of anchorEntries) {
43
+ for (let axisIdx = 0; axisIdx < spec.axesSpec.length; axisIdx++) {
44
+ const axis = spec.axesSpec[axisIdx];
45
+ const key = axisKey(axis);
46
+ this.axes.set(key, { anchor: anchorId, idx: axisIdx });
47
+ }
48
+ if (spec.domain !== undefined) {
49
+ const domainEntries = Object.entries(spec.domain);
50
+ domainEntries.sort((a, b) => a[0].localeCompare(b[0]));
51
+
52
+ this.domainPackToAnchor.set(JSON.stringify(domainEntries), anchorId);
53
+ this.domainPacks.push(domainEntries.map(([dKey]) => dKey));
54
+
55
+ for (const [dKey, dValue] of domainEntries) {
56
+ const key = domainKey(dKey, dValue);
57
+ this.domains.set(key, anchorId);
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Derives an anchored column identifier from a column specification
65
+ * Replaces domain values and axes with anchored references when possible
66
+ * @param spec Column specification to anchor
67
+ * @returns An anchored column identifier that can be used to identify columns similar to the input specification
68
+ */
69
+ derive(spec: PColumnSpec): APColumnId {
70
+ const result: APColumnId = {
71
+ name: spec.name,
72
+ axes: [],
73
+ };
74
+
75
+ let skipDomains: Set<string> | undefined = undefined;
76
+ if (spec.domain !== undefined) {
77
+ outer:
78
+ for (const domainPack of this.domainPacks) {
79
+ const dAnchor: string[][] = [];
80
+ for (const domainKey of domainPack) {
81
+ const dValue = spec.domain[domainKey];
82
+ if (dValue !== undefined)
83
+ dAnchor.push([domainKey, dValue]);
84
+ else
85
+ break outer;
86
+ }
87
+ const domainAnchor = this.domainPackToAnchor.get(JSON.stringify(dAnchor));
88
+ if (domainAnchor !== undefined) {
89
+ result.domainAnchor = domainAnchor;
90
+ skipDomains = new Set(domainPack);
91
+ break;
92
+ }
93
+ }
94
+ }
95
+
96
+ for (const [dKey, dValue] of Object.entries(spec.domain ?? {})) {
97
+ if (skipDomains !== undefined && skipDomains.has(dKey))
98
+ continue;
99
+ const key = domainKey(dKey, dValue);
100
+ const anchorId = this.domains.get(key);
101
+ result.domain ??= {};
102
+ result.domain[dKey] = anchorId ? { anchor: anchorId } : dValue;
103
+ }
104
+
105
+ result.axes = spec.axesSpec.map((axis) => {
106
+ const key = axisKey(axis);
107
+ const anchorAxisRef = this.axes.get(key);
108
+ return anchorAxisRef ?? axis;
109
+ });
110
+
111
+ return result;
112
+ }
113
+
114
+ /**
115
+ * Derives a canonicalized string representation of an anchored column identifier, can be used as a unique identifier for the column
116
+ * @param spec Column specification to anchor
117
+ * @returns A canonicalized string representation of the anchored column identifier
118
+ */
119
+ deriveString(spec: PColumnSpec): string {
120
+ const aId = this.derive(spec);
121
+ return canonicalize(aId)!;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Resolves anchored references in a column matcher to create a non-anchored matcher
127
+ *
128
+ * @param anchors - Record of anchor column specifications indexed by anchor ID
129
+ * @param matcher - An anchored column matcher containing references that need to be resolved
130
+ * @returns A non-anchored column matcher with all references resolved to actual values
131
+ */
132
+ export function resolveAnchors(anchors: Record<string, PColumnSpec>, matcher: APColumnSelector): PColumnSelector {
133
+ const result = { ...matcher };
134
+
135
+ if (result.domainAnchor !== undefined) {
136
+ const anchorSpec = anchors[result.domainAnchor];
137
+ if (!anchorSpec)
138
+ throw new Error(`Anchor "${result.domainAnchor}" not found`);
139
+
140
+ const anchorDomains = anchorSpec.domain || {};
141
+ result.domain = { ...anchorDomains, ...result.domain };
142
+ delete result.domainAnchor;
143
+ }
144
+
145
+ if (result.domain) {
146
+ const resolvedDomain: Record<string, string> = {};
147
+ for (const [key, value] of Object.entries(result.domain)) {
148
+ if (typeof value === 'string') {
149
+ resolvedDomain[key] = value;
150
+ } else {
151
+ // It's an AnchorDomainRef
152
+ const anchorSpec = anchors[value.anchor];
153
+ if (!anchorSpec)
154
+ throw new Error(`Anchor "${value.anchor}" not found for domain key "${key}"`);
155
+
156
+ if (!anchorSpec.domain || anchorSpec.domain[key] === undefined)
157
+ throw new Error(`Domain key "${key}" not found in anchor "${value.anchor}"`);
158
+
159
+ resolvedDomain[key] = anchorSpec.domain[key];
160
+ }
161
+ }
162
+ result.domain = resolvedDomain;
163
+ }
164
+
165
+ if (result.axes)
166
+ result.axes = result.axes.map((axis) => resolveAxisReference(anchors, axis));
167
+
168
+ return result as PColumnSelector;
169
+ }
170
+
171
+ /**
172
+ * Resolves an anchored axis reference to a concrete AxisId
173
+ */
174
+ function resolveAxisReference(anchors: Record<string, PColumnSpec>, axisRef: AAxisSelector): AxisSelector {
175
+ if (!isAnchorAxisRef(axisRef))
176
+ return axisRef;
177
+
178
+ // It's an anchored reference
179
+ const anchorId = axisRef.anchor;
180
+ const anchorSpec = anchors[anchorId];
181
+ if (!anchorSpec)
182
+ throw new Error(`Anchor "${anchorId}" not found for axis reference`);
183
+
184
+ if ('idx' in axisRef) {
185
+ // AnchorAxisRefByIdx
186
+ if (axisRef.idx < 0 || axisRef.idx >= anchorSpec.axesSpec.length)
187
+ throw new Error(`Axis index ${axisRef.idx} out of bounds for anchor "${anchorId}"`);
188
+ return anchorSpec.axesSpec[axisRef.idx];
189
+ } else if ('name' in axisRef) {
190
+ // AnchorAxisRefByName
191
+ const matches = anchorSpec.axesSpec.filter((axis) => axis.name === axisRef.name);
192
+ if (matches.length > 1)
193
+ throw new Error(`Multiple axes with name "${axisRef.name}" found in anchor "${anchorId}"`);
194
+ if (matches.length === 0)
195
+ throw new Error(`Axis with name "${axisRef.name}" not found in anchor "${anchorId}"`);
196
+ return matches[0];
197
+ } else if ('id' in axisRef) {
198
+ // AnchorAxisRefByMatcher
199
+ const matches = anchorSpec.axesSpec.filter((axis) => matchAxisId(axisRef.id, getAxisId(axis)));
200
+ if (matches.length > 1)
201
+ throw new Error(`Multiple matching axes found for matcher in anchor "${anchorId}"`);
202
+ if (matches.length === 0)
203
+ throw new Error(`No matching axis found for matcher in anchor "${anchorId}"`);
204
+ return matches[0];
205
+ }
206
+
207
+ throw new Error(`Unsupported axis reference type`);
208
+ }
209
+
210
+ /**
211
+ * Type guard to check if a value is an anchored axis reference
212
+ */
213
+ function isAnchorAxisRef(value: AAxisSelector): value is AnchorAxisRef {
214
+ return typeof value === 'object' && 'anchor' in value;
215
+ }
@@ -0,0 +1,3 @@
1
+ export * from './anchored_id';
2
+ export * from './spec';
3
+ export * from './selectors';
@@ -0,0 +1,209 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { matchPColumn, matchAxis } from './selectors';
3
+ import type { PColumnSpec, AxisSpec, AxisId } from './spec';
4
+
5
+ describe('matchPColumn', () => {
6
+ // Test data
7
+ const testColumn: PColumnSpec = {
8
+ kind: 'PColumn',
9
+ name: 'testColumn',
10
+ valueType: 'Int',
11
+ domain: {
12
+ domain1: 'value1',
13
+ domain2: 'value2',
14
+ },
15
+ axesSpec: [
16
+ {
17
+ name: 'x',
18
+ type: 'String',
19
+ domain: {
20
+ key: 'xDomain',
21
+ },
22
+ } as AxisSpec,
23
+ {
24
+ name: 'y',
25
+ type: 'String',
26
+ domain: {
27
+ key: 'yDomain',
28
+ },
29
+ } as AxisSpec,
30
+ ],
31
+ annotations: {
32
+ anno1: 'value1',
33
+ anno2: 'value2',
34
+ },
35
+ };
36
+
37
+ test('matches on name', () => {
38
+ expect(matchPColumn(testColumn, { name: 'testColumn' })).toBe(true);
39
+ expect(matchPColumn(testColumn, { name: 'wrongName' })).toBe(false);
40
+ });
41
+
42
+ test('matches on name pattern', () => {
43
+ expect(matchPColumn(testColumn, { namePattern: 'test.*' })).toBe(true);
44
+ expect(matchPColumn(testColumn, { namePattern: 'wrong.*' })).toBe(false);
45
+ });
46
+
47
+ test('matches on value type', () => {
48
+ expect(matchPColumn(testColumn, { type: 'Int' })).toBe(true);
49
+ expect(matchPColumn(testColumn, { type: 'String' })).toBe(false);
50
+ expect(matchPColumn(testColumn, { type: ['Int', 'Double'] })).toBe(true);
51
+ expect(matchPColumn(testColumn, { type: ['String', 'Float'] })).toBe(false);
52
+ });
53
+
54
+ test('matches on domain', () => {
55
+ expect(matchPColumn(testColumn, { domain: { domain1: 'value1' } })).toBe(true);
56
+ expect(matchPColumn(testColumn, { domain: { domain1: 'wrongValue' } })).toBe(false);
57
+ expect(matchPColumn(testColumn, { domain: { nonExistentDomain: 'value' } })).toBe(false);
58
+ });
59
+
60
+ test('matches on axes', () => {
61
+ expect(matchPColumn(testColumn, {
62
+ axes: [
63
+ {
64
+ name: 'x',
65
+ type: 'String',
66
+ domain: {
67
+ key: 'xDomain',
68
+ },
69
+ },
70
+ {
71
+ name: 'y',
72
+ type: 'String',
73
+ domain: {
74
+ key: 'yDomain',
75
+ },
76
+ },
77
+ ],
78
+ })).toBe(true);
79
+
80
+ // Test partial match
81
+ expect(matchPColumn(testColumn, {
82
+ axes: [{
83
+ name: 'x',
84
+ domain: {
85
+ key: 'xDomain',
86
+ },
87
+ }],
88
+ partialAxesMatch: true,
89
+ })).toBe(true);
90
+
91
+ // Test failed match (wrong axis)
92
+ expect(matchPColumn(testColumn, {
93
+ axes: [{
94
+ name: 'z',
95
+ type: 'String',
96
+ domain: {
97
+ key: 'zDomain',
98
+ },
99
+ }],
100
+ partialAxesMatch: true,
101
+ })).toBe(false);
102
+
103
+ // Test failed match (count mismatch with exact matching)
104
+ expect(matchPColumn(testColumn, {
105
+ axes: [{
106
+ name: 'x',
107
+ type: 'String',
108
+ domain: {
109
+ key: 'xDomain',
110
+ },
111
+ }],
112
+ })).toBe(false);
113
+ });
114
+
115
+ test('matches on annotations', () => {
116
+ expect(matchPColumn(testColumn, { annotations: { anno1: 'value1' } })).toBe(true);
117
+ expect(matchPColumn(testColumn, { annotations: { anno1: 'wrongValue' } })).toBe(false);
118
+ expect(matchPColumn(testColumn, { annotations: { nonExistentAnno: 'value' } })).toBe(false);
119
+ });
120
+
121
+ test('matches on annotation patterns', () => {
122
+ expect(matchPColumn(testColumn, { annotationPatterns: { anno1: 'value\\d' } })).toBe(true);
123
+ expect(matchPColumn(testColumn, { annotationPatterns: { anno1: 'wrong.*' } })).toBe(false);
124
+ expect(matchPColumn(testColumn, { annotationPatterns: { nonExistentAnno: '.*' } })).toBe(false);
125
+ });
126
+
127
+ test('matches on multiple criteria', () => {
128
+ expect(matchPColumn(testColumn, {
129
+ name: 'testColumn',
130
+ type: 'Int',
131
+ domain: { domain1: 'value1' },
132
+ annotations: { anno1: 'value1' },
133
+ })).toBe(true);
134
+
135
+ expect(matchPColumn(testColumn, {
136
+ name: 'testColumn',
137
+ type: 'Int',
138
+ domain: { domain1: 'wrongValue' }, // This will fail
139
+ annotations: { anno1: 'value1' },
140
+ })).toBe(false);
141
+ });
142
+ });
143
+
144
+ describe('matchAxis', () => {
145
+ // Test data
146
+ const testAxis: AxisId = {
147
+ name: 'testAxis',
148
+ type: 'String',
149
+ domain: {
150
+ key1: 'value1',
151
+ key2: 'value2',
152
+ },
153
+ };
154
+
155
+ test('matches on axis name', () => {
156
+ expect(matchAxis({ name: 'testAxis' }, testAxis)).toBe(true);
157
+ expect(matchAxis({ name: 'wrongAxis' }, testAxis)).toBe(false);
158
+ });
159
+
160
+ test('matches on axis type', () => {
161
+ expect(matchAxis({ type: 'String' }, testAxis)).toBe(true);
162
+ expect(matchAxis({ type: 'Int' }, testAxis)).toBe(false);
163
+ expect(matchAxis({ type: ['String', 'Int'] }, testAxis)).toBe(true);
164
+ expect(matchAxis({ type: ['Int', 'Double'] }, testAxis)).toBe(false);
165
+ });
166
+
167
+ test('matches on axis domain', () => {
168
+ expect(matchAxis({ domain: { key1: 'value1' } }, testAxis)).toBe(true);
169
+ expect(matchAxis({ domain: { key1: 'wrongValue' } }, testAxis)).toBe(false);
170
+ expect(matchAxis({ domain: { nonExistentKey: 'value' } }, testAxis)).toBe(false);
171
+ });
172
+
173
+ test('matches on multiple axis criteria', () => {
174
+ expect(matchAxis(
175
+ {
176
+ name: 'testAxis',
177
+ type: 'String',
178
+ domain: { key1: 'value1' },
179
+ },
180
+ testAxis,
181
+ )).toBe(true);
182
+
183
+ expect(matchAxis(
184
+ {
185
+ name: 'testAxis',
186
+ type: 'String',
187
+ domain: { key1: 'wrongValue' }, // This will fail
188
+ },
189
+ testAxis,
190
+ )).toBe(false);
191
+
192
+ expect(matchAxis(
193
+ {
194
+ name: 'testAxis',
195
+ type: 'Int', // This will fail
196
+ domain: { key1: 'value1' },
197
+ },
198
+ testAxis,
199
+ )).toBe(false);
200
+ });
201
+
202
+ test('matches with empty domain criteria', () => {
203
+ expect(matchAxis({ domain: {} }, testAxis)).toBe(true);
204
+ });
205
+
206
+ test('matches with partial domain criteria', () => {
207
+ expect(matchAxis({ domain: { key1: 'value1' } }, testAxis)).toBe(true);
208
+ });
209
+ });
@@ -0,0 +1,267 @@
1
+ import { isPColumnSpec, type PObjectSpec } from '../../../pool';
2
+ import type { AxisId, PColumnSpec, ValueType } from './spec';
3
+ import { getAxisId, matchAxisId } from './spec';
4
+
5
+ /**
6
+ * Defines a pattern for matching axes within the PFrame data model.
7
+ *
8
+ * AxisSelector provides a flexible way to identify axes based on their
9
+ * properties. All fields are optional, allowing for partial matching.
10
+ * When multiple properties are specified, all must match for an axis
11
+ * to be selected (logical AND).
12
+ *
13
+ * This interface is used in various selection and matching operations
14
+ * throughout the PFrame system, such as column queries and axis lookups.
15
+ */
16
+ export interface AxisSelector {
17
+ /**
18
+ * Optional value type to match against.
19
+ * When specified, only axes with this exact type will match.
20
+ * Can be a single type or an array of types to match against any of them.
21
+ * Valid types include: 'Int', 'Long', 'Float', 'Double', 'String', 'Bytes'.
22
+ */
23
+ type?: ValueType | ValueType[];
24
+
25
+ /**
26
+ * Optional name to match against.
27
+ * When specified, only axes with this exact name will match.
28
+ */
29
+ name?: string;
30
+
31
+ /**
32
+ * Optional domain key-value pairs to match against.
33
+ * Domains provide additional context to uniquely identify an axis beyond its name and type.
34
+ * When specified, an axis will match only if it contains all the key-value pairs defined here.
35
+ * An axis with additional domain entries not present in this selector will still match.
36
+ */
37
+ domain?: Record<string, string>;
38
+ }
39
+
40
+ /**
41
+ * Reference to an axis by its numerical index within the anchor column's axes array
42
+ * Format: [anchorId, axisIndex]
43
+ */
44
+ export type AnchorAxisRefByIdx = { anchor: string; idx: number };
45
+
46
+ /**
47
+ * Reference to an axis by its name within the anchor column
48
+ * Format: [anchorId, axisName]
49
+ */
50
+ export type AnchorAxisRefByName = { anchor: string; name: string };
51
+
52
+ /**
53
+ * Reference to an axis using an AxisId matcher within the anchor
54
+ * Format: [anchorId, axisMatcher]
55
+ */
56
+ export type AnchorAxisRefByMatcher = { anchor: string; id: AxisId };
57
+
58
+ /**
59
+ * Basic anchor axis reference that can be either by index or a direct AxisId
60
+ */
61
+ export type AnchorAxisIdOrRefBasic = AnchorAxisRefByIdx | AxisId;
62
+
63
+ /** Union of all possible ways to reference an axis in an anchored context */
64
+ export type AnchorAxisRef = AnchorAxisRefByIdx | AnchorAxisRefByName | AnchorAxisRefByMatcher;
65
+
66
+ /** Reference to a domain value through an anchor */
67
+ export type AnchorDomainRef = { anchor: string };
68
+
69
+ /**
70
+ * Domain value that can be either a direct string value or a reference to a domain through an anchor
71
+ * Used to establish domain context that can be resolved relative to other anchored columns
72
+ */
73
+ export type ADomain = string | AnchorDomainRef;
74
+ /**
75
+ * Axis identifier that can be either a direct AxisId or a reference to an axis through an anchor
76
+ * Allows referring to axes in a way that can be resolved in different contexts
77
+ */
78
+ export type AAxisSelector = AxisSelector | AnchorAxisRef;
79
+
80
+ /**
81
+ * Match resolution strategy for PColumns
82
+ * Specifies how to handle when multiple columns match the criteria
83
+ * (default is "expectSingle")
84
+ */
85
+ export type AnchoredColumnMatchStrategy = 'expectSingle' | 'expectMultiple' | 'takeFirst';
86
+
87
+ /**
88
+ * Matcher for PColumns in an anchored context
89
+ * Supports partial matching on axes, allowing for flexible column discovery
90
+ */
91
+ export interface APColumnSelector {
92
+ /** Optional name of the column to match; can't be used together with namePattern */
93
+ name?: string;
94
+ /** Optional regexp pattern for column name matching; can't be used together with name */
95
+ namePattern?: string;
96
+ /** Optional value type to match. If an array is provided, matches if the column's type is any of the specified types */
97
+ type?: ValueType | ValueType[];
98
+ /** If specified, the domain values must be anchored to this anchor */
99
+ domainAnchor?: string;
100
+ /** Optional domain values to match, can include anchored references, if domainAnchor is specified,
101
+ * interpreted as additional domains to domain from the anchor */
102
+ domain?: Record<string, ADomain>;
103
+ /** Optional axes to match, can include anchored references */
104
+ axes?: AAxisSelector[];
105
+ /** When true, allows matching if only a subset of axes match */
106
+ partialAxesMatch?: boolean;
107
+ /** Optional annotations to match with exact values */
108
+ annotations?: Record<string, string>;
109
+ /** Optional annotation patterns to match with regex patterns */
110
+ annotationPatterns?: Record<string, string>;
111
+ /** Match resolution strategy, default is "expectSingle" */
112
+ matchStrategy?: AnchoredColumnMatchStrategy;
113
+ }
114
+
115
+ /**
116
+ * Matcher for PColumns in a non-anchored context
117
+ */
118
+ export interface PColumnSelector extends APColumnSelector {
119
+ domainAnchor?: never;
120
+ domain?: Record<string, string>;
121
+ axes?: AxisSelector[];
122
+ }
123
+
124
+ /**
125
+ * Strict identifier for PColumns in an anchored context
126
+ * Unlike APColumnMatcher, this requires exact matches on domain and axes
127
+ */
128
+ export interface APColumnId extends APColumnSelector {
129
+ /** Name is required for exact column identification */
130
+ name: string;
131
+ /** No namePattern in ID */
132
+ namePattern?: never;
133
+ /** Type is not used in exact column identification */
134
+ type?: never;
135
+ /** Full axes specification using only basic references */
136
+ axes: AnchorAxisIdOrRefBasic[];
137
+ /** Partial axes matching is not allowed for exact identification */
138
+ partialAxesMatch?: never;
139
+ /** Annotations are not used in exact column identification */
140
+ annotations?: never;
141
+ /** Annotation patterns are not used in exact column identification */
142
+ annotationPatterns?: never;
143
+ /** "Id" implies single match strategy */
144
+ matchStrategy?: never;
145
+ }
146
+
147
+ /**
148
+ * Determines if an axis ID matches an axis selector.
149
+ *
150
+ * @param selector - The selector with criteria to match against
151
+ * @param axis - The AxisId to check against the selector
152
+ * @returns true if the AxisId matches all specified criteria in the selector, false otherwise
153
+ */
154
+ export function matchAxis(selector: AxisSelector, axis: AxisId): boolean {
155
+ // Match name if specified
156
+ if (selector.name !== undefined && selector.name !== axis.name)
157
+ return false;
158
+
159
+ // Match type if specified
160
+ if (selector.type !== undefined) {
161
+ if (Array.isArray(selector.type)) {
162
+ if (!selector.type.includes(axis.type))
163
+ return false;
164
+ } else if (selector.type !== axis.type) {
165
+ return false;
166
+ }
167
+ }
168
+
169
+ // Match domain if specified - using existing logic from matchAxisId
170
+ if (selector.domain !== undefined) {
171
+ const axisDomain = axis.domain || {};
172
+ for (const [key, value] of Object.entries(selector.domain))
173
+ if (axisDomain[key] !== value)
174
+ return false;
175
+ }
176
+
177
+ return true;
178
+ }
179
+
180
+ /**
181
+ * Determines if a given PColumnSpec matches a selector.
182
+ *
183
+ * @param pcolumn - The PColumnSpec to check against the selector
184
+ * @param selector - The selector criteria to match against
185
+ * @returns true if the PColumnSpec matches all criteria in the selector, false otherwise
186
+ */
187
+ export function matchPColumn(pcolumn: PColumnSpec, selector: PColumnSelector): boolean {
188
+ // Match name if specified
189
+ if (selector.name !== undefined && pcolumn.name !== selector.name)
190
+ return false;
191
+
192
+ // Match name pattern if specified
193
+ if (selector.namePattern !== undefined && !new RegExp(selector.namePattern).test(pcolumn.name))
194
+ return false;
195
+
196
+ // Match type if specified
197
+ if (selector.type !== undefined) {
198
+ if (Array.isArray(selector.type)) {
199
+ if (!selector.type.includes(pcolumn.valueType))
200
+ return false;
201
+ } else if (selector.type !== pcolumn.valueType) {
202
+ return false;
203
+ }
204
+ }
205
+
206
+ // Match domain if specified
207
+ if (selector.domain !== undefined) {
208
+ const columnDomain = pcolumn.domain || {};
209
+ for (const [key, value] of Object.entries(selector.domain))
210
+ if (columnDomain[key] !== value)
211
+ return false;
212
+ }
213
+
214
+ // Match axes if specified
215
+ if (selector.axes !== undefined) {
216
+ const pcolumnAxes = pcolumn.axesSpec.map(getAxisId);
217
+
218
+ if (selector.partialAxesMatch) {
219
+ // For partial matching, all selector axes must match at least one column axis
220
+ for (const selectorAxis of selector.axes)
221
+ if (!pcolumnAxes.some((columnAxis) => matchAxis(selectorAxis, columnAxis)))
222
+ return false;
223
+ } else {
224
+ // For exact matching, column must have the same number of axes and all must match
225
+ if (pcolumnAxes.length !== selector.axes.length)
226
+ return false;
227
+
228
+ // Each selector axis must match a corresponding column axis
229
+ for (let i = 0; i < selector.axes.length; i++)
230
+ if (!matchAxis(selector.axes[i], pcolumnAxes[i]))
231
+ return false;
232
+ }
233
+ }
234
+
235
+ // Match annotations if specified
236
+ if (selector.annotations !== undefined) {
237
+ const columnAnnotations = pcolumn.annotations || {};
238
+ for (const [key, value] of Object.entries(selector.annotations))
239
+ if (columnAnnotations[key] !== value)
240
+ return false;
241
+ }
242
+
243
+ // Match annotation patterns if specified
244
+ if (selector.annotationPatterns !== undefined) {
245
+ const columnAnnotations = pcolumn.annotations || {};
246
+ for (const [key, pattern] of Object.entries(selector.annotationPatterns)) {
247
+ const value = columnAnnotations[key];
248
+ if (value === undefined || !new RegExp(pattern).test(value))
249
+ return false;
250
+ }
251
+ }
252
+
253
+ return true;
254
+ }
255
+
256
+ /**
257
+ * Convert a predicate or array of selectors to a single predicate function
258
+ * @param predicateOrSelectors - Either a function that takes a PColumnSpec and returns a boolean,
259
+ * or an array of PColumnSelectors, or a single PColumnSelector
260
+ * @returns A function that takes a PColumnSpec and returns a boolean
261
+ */
262
+ export function selectorsToPredicate(predicateOrSelectors: PColumnSelector | PColumnSelector[]): ((spec: PObjectSpec) => boolean) {
263
+ if (Array.isArray(predicateOrSelectors))
264
+ return (spec) => predicateOrSelectors.some((selector) => isPColumnSpec(spec) && matchPColumn(spec, selector));
265
+ else
266
+ return (spec) => isPColumnSpec(spec) && matchPColumn(spec, predicateOrSelectors);
267
+ }