@malloy-publisher/sdk 0.0.152 → 0.0.155

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@malloy-publisher/sdk",
3
3
  "description": "Malloy Publisher SDK",
4
- "version": "0.0.152",
4
+ "version": "0.0.155",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs.js",
7
7
  "module": "dist/index.es.js",
@@ -3,7 +3,10 @@ import * as Malloy from "@malloydata/malloy-interfaces";
3
3
  import { Box, Paper, Stack, Typography } from "@mui/material";
4
4
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
5
  import { RawNotebook } from "../../client";
6
- import { useDimensionalFilterRangeData } from "../../hooks/useDimensionalFilterRangeData";
6
+ import {
7
+ getDimensionKey,
8
+ useDimensionalFilterRangeData,
9
+ } from "../../hooks/useDimensionalFilterRangeData";
7
10
  import {
8
11
  FilterSelection,
9
12
  useDimensionFilters,
@@ -121,11 +124,13 @@ export default function Notebook({
121
124
  [filterStates, getActiveFilters],
122
125
  );
123
126
 
124
- // Create a map of dimension name -> source name for quick lookup
127
+ // Create a map of dimension key -> source name for quick lookup
128
+ // Using composite keys (source:dimensionName) to avoid collisions
125
129
  const dimensionToSourceMap = useMemo(() => {
126
130
  const map = new Map<string, string>();
127
131
  for (const spec of dimensionSpecs) {
128
- map.set(spec.dimensionName, spec.source);
132
+ const key = getDimensionKey(spec);
133
+ map.set(key, spec.source);
129
134
  }
130
135
  return map;
131
136
  }, [dimensionSpecs]);
@@ -212,16 +217,12 @@ export default function Notebook({
212
217
  new Set<string>();
213
218
 
214
219
  // Filter to only include those matching this query's source or joined sources
220
+ // FilterSelection now includes source, so we can check directly
215
221
  const filtersForSource = querySourceName
216
222
  ? filtersToApply.filter((filter) => {
217
- const filterSourceName =
218
- dimensionToSourceMap.get(
219
- filter.dimensionName,
220
- );
221
- if (!filterSourceName) return false;
222
223
  return (
223
- filterSourceName === querySourceName ||
224
- joinedSources.has(filterSourceName)
224
+ filter.source === querySourceName ||
225
+ joinedSources.has(filter.source)
225
226
  );
226
227
  })
227
228
  : [];
@@ -374,10 +375,10 @@ export default function Notebook({
374
375
  }
375
376
  }, [activeFilters, isExecuting, executeCells]);
376
377
 
377
- // Handle filter change
378
+ // Handle filter change using composite key
378
379
  const handleFilterChange = useCallback(
379
- (dimensionName: string) => (selection: FilterSelection | null) => {
380
- updateFilter(dimensionName, selection);
380
+ (key: string) => (selection: FilterSelection | null) => {
381
+ updateFilter(key, selection);
381
382
  },
382
383
  [updateFilter],
383
384
  );
@@ -427,11 +428,9 @@ export default function Notebook({
427
428
  }}
428
429
  >
429
430
  {dimensionSpecs.map((spec) => {
430
- const values =
431
- filterValuesData.get(spec.dimensionName) || [];
432
- const filterState = filterStates.get(
433
- spec.dimensionName,
434
- );
431
+ const key = getDimensionKey(spec);
432
+ const values = filterValuesData.get(key) || [];
433
+ const filterState = filterStates.get(key);
435
434
  // Skip Retrieval filters if no retrievalFn provided
436
435
  if (
437
436
  spec.filterType === "Retrieval" &&
@@ -441,14 +440,12 @@ export default function Notebook({
441
440
  }
442
441
 
443
442
  return (
444
- <Box key={spec.dimensionName}>
443
+ <Box key={key}>
445
444
  <DimensionFilter
446
445
  spec={spec}
447
446
  values={values}
448
447
  selection={filterState?.selection}
449
- onChange={handleFilterChange(
450
- spec.dimensionName,
451
- )}
448
+ onChange={handleFilterChange(key)}
452
449
  retrievalFn={retrievalFn}
453
450
  />
454
451
  </Box>
@@ -265,6 +265,7 @@ export function DimensionFilter({
265
265
  if (value1) {
266
266
  onChange({
267
267
  dimensionName: spec.dimensionName,
268
+ source: spec.source,
268
269
  matchType: newMatchType,
269
270
  value: value1,
270
271
  ...(requiresTwoValues(newMatchType) && value2 && { value2 }),
@@ -294,6 +295,7 @@ export function DimensionFilter({
294
295
  ) {
295
296
  onChange({
296
297
  dimensionName: spec.dimensionName,
298
+ source: spec.source,
297
299
  matchType,
298
300
  value: newValue1,
299
301
  ...(needsTwoValues && newValue2 && { value2: newValue2 }),
@@ -263,16 +263,20 @@ export function extractDimensionSpecs(
263
263
  */
264
264
  export function generateFilterClause(
265
265
  activeFilters: FilterSelection[],
266
- dimensionToSourceMap: Map<string, string>,
266
+ _dimensionToSourceMap: Map<string, string>,
267
267
  querySourceName: string | null,
268
268
  ): string {
269
269
  if (activeFilters.length === 0) return "";
270
270
 
271
271
  const conditions = activeFilters
272
272
  .map((selection) => {
273
- const { dimensionName, matchType, value, value2 } = selection;
274
- // Get the source name for this dimension
275
- const filterSourceName = dimensionToSourceMap.get(dimensionName);
273
+ const {
274
+ dimensionName,
275
+ source: filterSourceName,
276
+ matchType,
277
+ value,
278
+ value2,
279
+ } = selection;
276
280
  // Only use source.dimension format if the filter's source is different from the query's source
277
281
  // (i.e., it's a joined source, not the main source)
278
282
  const needsSourcePrefix =
@@ -22,6 +22,8 @@ export {
22
22
 
23
23
  export {
24
24
  useDimensionalFilterRangeData,
25
+ getDimensionKey,
26
+ makeDimensionKey,
25
27
  type DimensionalFilterRangeDataResult,
26
28
  type DimensionSpec,
27
29
  type DimensionValue,
@@ -1,5 +1,8 @@
1
1
  import { useCallback, useEffect, useState } from "react";
2
- import { DimensionSpec } from "./useDimensionalFilterRangeData";
2
+ import {
3
+ DimensionSpec,
4
+ getDimensionKey,
5
+ } from "./useDimensionalFilterRangeData";
3
6
 
4
7
  /**
5
8
  * Match types for filtering dimensions
@@ -29,6 +32,8 @@ export type FilterValue = FilterValuePrimitive | FilterValuePrimitive[];
29
32
  */
30
33
  export interface FilterSelection {
31
34
  dimensionName: string;
35
+ /** Source name - required to uniquely identify filters when same dimension name exists in multiple sources */
36
+ source: string;
32
37
  matchType: MatchType;
33
38
  value: FilterValue;
34
39
  value2?: FilterValuePrimitive; // For "Between" match type
@@ -54,15 +59,12 @@ export interface UseDimensionFiltersParams {
54
59
  * Result from the useDimensionFilters hook
55
60
  */
56
61
  export interface UseDimensionFiltersResult {
57
- /** Current filter states */
62
+ /** Current filter states, keyed by composite key (source:dimensionName) */
58
63
  filterStates: Map<string, DimensionFilterState>;
59
- /** Update a filter selection */
60
- updateFilter: (
61
- dimensionName: string,
62
- selection: FilterSelection | null,
63
- ) => void;
64
- /** Clear a specific filter */
65
- clearFilter: (dimensionName: string) => void;
64
+ /** Update a filter selection using composite key */
65
+ updateFilter: (key: string, selection: FilterSelection | null) => void;
66
+ /** Clear a specific filter using composite key */
67
+ clearFilter: (key: string) => void;
66
68
  /** Clear all filters */
67
69
  clearAllFilters: () => void;
68
70
  /** Get active filters (with selections) */
@@ -207,13 +209,14 @@ export function useDimensionFilters(
207
209
  ): UseDimensionFiltersResult {
208
210
  const { dimensionSpecs } = params;
209
211
 
210
- // Initialize filter states
212
+ // Initialize filter states using composite keys (source:dimensionName)
211
213
  const [filterStates, setFilterStates] = useState<
212
214
  Map<string, DimensionFilterState>
213
215
  >(() => {
214
216
  const initialStates = new Map<string, DimensionFilterState>();
215
217
  dimensionSpecs.forEach((spec) => {
216
- initialStates.set(spec.dimensionName, {
218
+ const key = getDimensionKey(spec);
219
+ initialStates.set(key, {
217
220
  spec,
218
221
  selection: null,
219
222
  });
@@ -227,9 +230,10 @@ export function useDimensionFilters(
227
230
  const newStates = new Map<string, DimensionFilterState>();
228
231
 
229
232
  dimensionSpecs.forEach((spec) => {
233
+ const key = getDimensionKey(spec);
230
234
  // Preserve existing selection if the dimension already exists
231
- const existingState = prevStates.get(spec.dimensionName);
232
- newStates.set(spec.dimensionName, {
235
+ const existingState = prevStates.get(key);
236
+ newStates.set(key, {
233
237
  spec,
234
238
  selection: existingState?.selection ?? null,
235
239
  });
@@ -239,15 +243,15 @@ export function useDimensionFilters(
239
243
  });
240
244
  }, [dimensionSpecs]);
241
245
 
242
- // Update a filter selection
246
+ // Update a filter selection using composite key
243
247
  const updateFilter = useCallback(
244
- (dimensionName: string, selection: FilterSelection | null) => {
248
+ (key: string, selection: FilterSelection | null) => {
245
249
  setFilterStates((prevStates) => {
246
250
  const newStates = new Map(prevStates);
247
- const existingState = newStates.get(dimensionName);
251
+ const existingState = newStates.get(key);
248
252
 
249
253
  if (existingState) {
250
- newStates.set(dimensionName, {
254
+ newStates.set(key, {
251
255
  ...existingState,
252
256
  selection,
253
257
  });
@@ -259,10 +263,10 @@ export function useDimensionFilters(
259
263
  [],
260
264
  );
261
265
 
262
- // Clear a specific filter
266
+ // Clear a specific filter using composite key
263
267
  const clearFilter = useCallback(
264
- (dimensionName: string) => {
265
- updateFilter(dimensionName, null);
268
+ (key: string) => {
269
+ updateFilter(key, null);
266
270
  },
267
271
  [updateFilter],
268
272
  );
@@ -34,6 +34,24 @@ export interface DimensionSpec {
34
34
  values?: string[];
35
35
  }
36
36
 
37
+ /**
38
+ * Generates a unique key from source and dimension name strings.
39
+ * This prevents collisions when the same dimension name exists in different sources.
40
+ */
41
+ export function makeDimensionKey(
42
+ source: string,
43
+ dimensionName: string,
44
+ ): string {
45
+ return `${source}:${dimensionName}`;
46
+ }
47
+
48
+ /**
49
+ * Generates a unique key for a dimension by combining source and dimension name.
50
+ */
51
+ export function getDimensionKey(spec: DimensionSpec): string {
52
+ return makeDimensionKey(spec.source, spec.dimensionName);
53
+ }
54
+
37
55
  /**
38
56
  * Value information for a dimension
39
57
  */
@@ -43,7 +61,7 @@ export interface DimensionValue {
43
61
  }
44
62
 
45
63
  /**
46
- * Result type mapping dimension names to their values
64
+ * Result type mapping dimension keys (source:dimensionName) to their values
47
65
  */
48
66
  export type DimensionValues = Map<string, DimensionValue[]>;
49
67
 
@@ -236,13 +254,7 @@ function buildDimensionalIndexQuery(
236
254
 
237
255
  // Filter activeFilters to only include those for this source
238
256
  const filtersForSource =
239
- activeFilters?.filter((f) => {
240
- // Find the spec for this filter's dimension
241
- const spec = dimensionSpecs.find(
242
- (s) => s.dimensionName === f.dimensionName,
243
- );
244
- return spec?.source === source;
245
- }) || [];
257
+ activeFilters?.filter((f) => f.source === source) || [];
246
258
 
247
259
  // Generate WHERE conditions from active filters (without 'where' keyword)
248
260
  const whereConditions =
@@ -332,9 +344,10 @@ function parseIndexQueryResult(
332
344
  ): { values: DimensionValues; noRowsMatchedFilter: boolean } {
333
345
  const dimensionValues = new Map<string, DimensionValue[]>();
334
346
 
335
- // Initialize empty arrays for all dimensions
347
+ // Initialize empty arrays for all dimensions using composite keys
336
348
  for (const spec of dimensionSpecs) {
337
- dimensionValues.set(spec.dimensionName, []);
349
+ const key = getDimensionKey(spec);
350
+ dimensionValues.set(key, []);
338
351
  }
339
352
 
340
353
  // Parse the result JSON if it's a string
@@ -460,6 +473,8 @@ function parseIndexQueryResult(
460
473
  continue;
461
474
  }
462
475
 
476
+ const specKey = getDimensionKey(spec);
477
+
463
478
  if (spec.filterType === "Star") {
464
479
  // For Star filter, we want all distinct values with their weights
465
480
  // String fields return individual fieldValue entries
@@ -474,7 +489,7 @@ function parseIndexQueryResult(
474
489
  (b.count ?? 0) - (a.count ?? 0),
475
490
  ); // Sort by count descending
476
491
 
477
- dimensionValues.set(spec.dimensionName, values);
492
+ dimensionValues.set(specKey, values);
478
493
  } else if (spec.filterType === "MinMax") {
479
494
  // For MinMax filter, numeric fields return a range in fieldValue
480
495
  // Format is typically "min to max"
@@ -504,7 +519,7 @@ function parseIndexQueryResult(
504
519
  { value: parseFloat(rangeParts[0]) },
505
520
  { value: parseFloat(rangeParts[1]) },
506
521
  ];
507
- dimensionValues.set(spec.dimensionName, values);
522
+ dimensionValues.set(specKey, values);
508
523
  } else {
509
524
  console.warn(
510
525
  `Could not parse numeric range for ${spec.dimensionName}: ${rangeString}`,
@@ -546,7 +561,7 @@ function parseIndexQueryResult(
546
561
  { value: new Date(rangeParts[0].trim()) },
547
562
  { value: new Date(rangeParts[1].trim()) },
548
563
  ];
549
- dimensionValues.set(spec.dimensionName, values);
564
+ dimensionValues.set(specKey, values);
550
565
  } else {
551
566
  console.warn(
552
567
  `Could not parse date range for ${spec.dimensionName}: ${rangeString}`,
@@ -675,10 +690,11 @@ export function useDimensionalFilterRangeData(
675
690
  ],
676
691
  queryFn: async () => {
677
692
  if (!shouldExecuteQuery) {
678
- // Return empty map if no query needed
693
+ // Return empty map if no query needed (using composite keys)
679
694
  const emptyMap = new Map<string, DimensionValue[]>();
680
695
  for (const spec of dimensionSpecs) {
681
- emptyMap.set(spec.dimensionName, []);
696
+ const key = getDimensionKey(spec);
697
+ emptyMap.set(key, []);
682
698
  }
683
699
  return { values: emptyMap, noRowsMatchedFilter: false };
684
700
  }
@@ -728,15 +744,16 @@ export function useDimensionalFilterRangeData(
728
744
  const mergedData = useMemo(() => {
729
745
  const result = new Map<string, DimensionValue[]>();
730
746
 
731
- // Initialize with static values or empty arrays
747
+ // Initialize with static values or empty arrays using composite keys
732
748
  for (const spec of dimensionSpecs) {
749
+ const key = getDimensionKey(spec);
733
750
  if (spec.values) {
734
751
  result.set(
735
- spec.dimensionName,
752
+ key,
736
753
  spec.values.map((v) => ({ value: v })),
737
754
  );
738
755
  } else {
739
- result.set(spec.dimensionName, []);
756
+ result.set(key, []);
740
757
  }
741
758
  }
742
759