Skip to content

Commit d4f4cc9

Browse files
authored
Add support for fields tool (#253)
* Add process fields function * Parse tables from cards * Combine all metadata processing * Parallelize metadata processor * Remove unused metadata process functions
1 parent a5fc2ec commit d4f4cc9

File tree

7 files changed

+194
-21
lines changed

7 files changed

+194
-21
lines changed

apps/src/metabase/appState.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { addNativeEventListener, RPCs, configs, renderString, getParsedIframeInfo, unsubscribe, captureEvent, GLOBAL_EVENTS, processCards, processDBSchema } from "web";
1+
import { addNativeEventListener, RPCs, configs, renderString, getParsedIframeInfo, unsubscribe, captureEvent, GLOBAL_EVENTS, processAllMetadata } from "web";
22
import { DefaultAppState } from "../base/appState";
33
import { MetabaseController } from "./appController";
44
import { DB_INFO_DEFAULT, metabaseInternalState } from "./defaultState";
@@ -206,8 +206,7 @@ export class MetabaseState extends DefaultAppState<MetabaseAppState> {
206206
// Perf caching
207207
if (!isCancelled()) {
208208
console.log('Running perf caching')
209-
processCards()
210-
processDBSchema()
209+
processAllMetadata()
211210
getDatabaseInfo(dbId)
212211
}
213212
})

apps/src/metabase/helpers/metabaseAPI.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ export const fetchFieldInfo = createAPI<{ field_id: number }>(
178178
'/api/field/{{field_id}}'
179179
);
180180

181+
export const fetchDatabaseFields = createAPI<{ db_id: number }>(
182+
'/api/database/{{db_id}}/fields'
183+
);
184+
181185
// Dataset Operations - For running SQL queries
182186
export const executeDatasetQuery = createAPI<{
183187
database: number;

apps/src/metabase/helpers/metabaseAPIHelpers.ts

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* from metabaseAPI.ts and state functions from metabaseStateAPI.ts.
66
*/
77

8-
import { map, get, isEmpty, flatMap, filter, sortBy, reverse, pick, omit } from 'lodash';
8+
import _, { map, get, isEmpty, flatMap, filter, sortBy, reverse, pick, omit } from 'lodash';
99
import { getTablesFromSqlRegex, TableAndSchema } from './parseSql';
1010
import { handlePromise, deterministicSample } from '../../common/utils';
1111
import { getCurrentUserInfo, getSelectedDbId } from './metabaseStateAPI';
@@ -29,7 +29,8 @@ import {
2929
fetchTableMetadata,
3030
fetchModels,
3131
fetchCard,
32-
fetchCards
32+
fetchCards,
33+
fetchDatabaseFields
3334
} from './metabaseAPI';
3435
import { Card, SearchApiResponse } from './types';
3536

@@ -262,8 +263,92 @@ export async function getAllCards() {
262263
});
263264

264265
console.log('Processed cards:', processedCards);
266+
const tables: Record<string, TableAndSchema> = {};
267+
_.forEach(processedCards, (card) => {
268+
getTablesFromSqlRegex(card.dataset_query?.native?.query || '').forEach((table: TableAndSchema) => {
269+
const tableKey = `${table.schema}.${table.name}`;
270+
if (!tables[tableKey]) {
271+
table.count = table.count || 1; // Initialize count if not present
272+
tables[tableKey] = table;
273+
} else {
274+
// If table already exists, merge the counts
275+
tables[tableKey].count = (tables[tableKey].count || 0) + (table.count || 0);
276+
}
277+
})
278+
})
279+
const relevantTables = _.chain(tables).values().sortBy('count').reverse().value();
280+
console.log('Tables from cards:', relevantTables);
281+
282+
return { cards: processedCards, tables: relevantTables };
283+
}
284+
285+
export async function getAllCardsLegacy() {
286+
const result = await getAllCards();
287+
return result.cards;
288+
}
289+
290+
export async function getAllFields() {
291+
// Get selected database ID
292+
const selectedDbId = await getSelectedDbId();
293+
294+
if (!selectedDbId) {
295+
console.log('[minusx] getAllFields - No database selected');
296+
return [];
297+
}
298+
299+
const fields = await handlePromise(
300+
fetchDatabaseFields({ db_id: selectedDbId }),
301+
"[minusx] Error getting all fields",
302+
[]
303+
);
304+
305+
console.log('[minusx] getAllFields - Total fields:', fields.length);
306+
307+
// Return top 1000 fields
308+
const limitedFields = fields.slice(0, 1000);
309+
310+
console.log('[minusx] getAllFields - Returning fields:', limitedFields.length);
311+
312+
return limitedFields;
313+
}
314+
315+
export async function getAllFieldsFiltered(tableNames: string[]) {
316+
if (!tableNames || tableNames.length === 0) {
317+
console.log('[minusx] getAllFieldsFiltered - No table names provided, returning empty array');
318+
return [];
319+
}
320+
321+
// Get selected database ID
322+
const selectedDbId = await getSelectedDbId();
323+
324+
if (!selectedDbId) {
325+
console.log('[minusx] getAllFieldsFiltered - No database selected');
326+
return [];
327+
}
328+
329+
const allFields = await handlePromise(
330+
fetchDatabaseFields({ db_id: selectedDbId }),
331+
"[minusx] Error getting all fields",
332+
[]
333+
);
334+
335+
console.log('[minusx] getAllFieldsFiltered - Total fields before filtering:', allFields.length);
336+
337+
// Create a set of table names for faster lookup
338+
const tableNameSet = new Set(tableNames);
339+
340+
// Filter fields to only those belonging to specified tables
341+
const filteredFields = filter(allFields, (field) => {
342+
const tableName = get(field, 'table_name');
343+
const tableSchema = get(field, 'schema');
344+
const fullTableName = tableSchema ? `${tableSchema}.${tableName}` : tableName;
345+
346+
return tableNameSet.has(tableName) || tableNameSet.has(fullTableName);
347+
});
348+
349+
console.log('[minusx] getAllFieldsFiltered - Fields after filtering:', filteredFields.length);
265350

266-
return processedCards;
351+
return filteredFields;
267352
}
268353

269354
export const getAllRelevantModelsForSelectedDb = async (dbId: number, forceRefreshModels: boolean = false): Promise<MetabaseModel[]> => {

apps/src/package.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
export { getAppStateConfigs } from "./appStateConfigs";
22
export { applyTableDiffs } from "./common/utils";
33
export { getTableContextYAML, filterTablesByCatalog } from "./metabase/helpers/catalog";
4-
export { getTableData, getDatabaseTablesAndModelsWithoutFields, getAllCards } from "./metabase/helpers/metabaseAPIHelpers";
4+
export { getTableData, getDatabaseTablesAndModelsWithoutFields, getAllCards, getAllCardsLegacy, getAllFields, getAllFieldsFiltered } from "./metabase/helpers/metabaseAPIHelpers";
55
export { fetchModelInfo } from "./metabase/helpers/metabaseAPI";
66
export { getAllTemplateTagsInQuery } from "./metabase/helpers/sqlQuery";
77
export { getModelsWithFields, getSelectedAndRelevantModels, modifySqlForMetabaseModels, replaceLLMFriendlyIdentifiersInSqlWithModels } from "./metabase/helpers/metabaseModels";
88
export { getCurrentQuery, getDashboardState } from "./metabase/helpers/metabaseStateAPI";
9-
export { subscribeMB, onMBSubscription } from "./metabase/helpers/stateSubscriptions";
9+
export { subscribeMB, onMBSubscription } from "./metabase/helpers/stateSubscriptions";
10+
export { getTablesFromSqlRegex } from "./metabase/helpers/parseSql";

web/src/helpers/LLM/remote.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getLLMResponse } from '../../app/api'
55
import { getApp } from '../app'
66
import { getState } from '../../state/store'
77
import { unset } from 'lodash'
8-
import { processCards, processDBSchema } from '../metadataProcessor'
8+
import { processAllMetadata } from '../metadataProcessor'
99

1010
export async function planActionsRemote({
1111
messages,
@@ -29,21 +29,21 @@ export async function planActionsRemote({
2929
unset(payload, 'tasks')
3030
}
3131

32-
const getCardsPromise = processCards()
33-
const getDBSchemaPromise = processDBSchema()
32+
const getAllMetadataPromise = processAllMetadata()
3433

3534
// Add metadata hashes for analyst mode (when both drMode and analystMode are enabled)
3635
if (deepResearch !== 'simple') {
3736
// Check if analyst mode is enabled by getting current state
3837
const currentState = getState();
3938
if (currentState.settings.drMode && currentState.settings.analystMode) {
4039
try {
41-
const cardsHash = await getCardsPromise;
42-
const dbSchemaHash = await getDBSchemaPromise;
40+
const { cardsHash, dbSchemaHash, fieldsHash } = await getAllMetadataPromise;
4341
// @ts-ignore
4442
payload.cardsHash = cardsHash;
4543
// @ts-ignore
4644
payload.dbSchemaHash = dbSchemaHash;
45+
// @ts-ignore
46+
payload.fieldsHash = fieldsHash;
4747
console.log('[minusx] Added metadata hashes to request for analyst mode');
4848
} catch (error) {
4949
console.warn('[minusx] Failed to fetch metadata for analyst mode:', error);

web/src/helpers/metadataProcessor.ts

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import { get } from 'lodash';
1212
import { setMetadataHash } from '../state/settings/reducer';
1313
import { getState } from '../state/store';
1414
import { dispatch } from '../state/dispatch';
15-
import { getAllCards, getDatabaseTablesAndModelsWithoutFields } from '../../../apps/src/metabase/helpers/metabaseAPIHelpers';
15+
import { getAllCards, getAllCardsLegacy, getDatabaseTablesAndModelsWithoutFields, getAllFields } from '../../../apps/src/metabase/helpers/metabaseAPIHelpers';
16+
import { fetchDatabaseFields } from '../../../apps/src/metabase/helpers/metabaseAPI';
17+
import { getSelectedDbId } from '../../../apps/src/metabase/helpers/metabaseStateAPI';
1618

1719
export interface MetadataItem {
1820
metadata_type: string;
@@ -149,11 +151,93 @@ async function processMetadataWithCaching(
149151
return currentHash
150152
}
151153

152-
export async function processCards() {
153-
return await processMetadataWithCaching('cards', getAllCards)
154-
}
155-
156-
export async function processDBSchema() {
157-
return await processMetadataWithCaching('dbSchema', getDatabaseTablesAndModelsWithoutFields)
154+
export async function processAllMetadata() {
155+
console.log('[minusx] Starting coordinated metadata processing with parallel API calls...')
156+
157+
// Step 1: Start all expensive API calls in parallel
158+
console.log('[minusx] Initiating parallel API calls...')
159+
const selectedDbId = await getSelectedDbId()
160+
161+
if (!selectedDbId) {
162+
throw new Error('No database selected for metadata processing')
163+
}
164+
165+
const [dbSchema, { cards, tables: referencedTables }, allFields] = await Promise.all([
166+
getDatabaseTablesAndModelsWithoutFields(),
167+
getAllCards(),
168+
fetchDatabaseFields({ db_id: selectedDbId })
169+
])
170+
171+
console.log('[minusx] All API calls completed. Processing data...')
172+
173+
// Step 2: Create sets for efficient lookup of existing tables
174+
const existingTableNames = new Set<string>()
175+
176+
// Add tables from dbSchema
177+
if (dbSchema.tables) {
178+
dbSchema.tables.forEach((table: any) => {
179+
const tableName = table.name
180+
const schemaName = table.schema || dbSchema.default_schema
181+
const fullName = schemaName ? `${schemaName}.${tableName}` : tableName
182+
183+
existingTableNames.add(tableName)
184+
existingTableNames.add(fullName)
185+
})
186+
}
187+
188+
// Add models from dbSchema
189+
if (dbSchema.models) {
190+
dbSchema.models.forEach((model: any) => {
191+
existingTableNames.add(model.name)
192+
})
193+
}
194+
195+
console.log('[minusx] Found existing tables/models:', existingTableNames.size)
196+
197+
// Step 3: Find intersection of referenced tables that actually exist
198+
const validReferencedTables = referencedTables.filter((table: any) => {
199+
const tableName = table.name
200+
const schemaName = table.schema
201+
const fullName = schemaName ? `${schemaName}.${tableName}` : tableName
202+
203+
return existingTableNames.has(tableName) || existingTableNames.has(fullName)
204+
})
205+
206+
console.log('[minusx] Valid referenced tables:', validReferencedTables.length, 'out of', referencedTables.length)
207+
208+
// Step 4: Filter fields in-memory using table names
209+
const validTableNames = new Set(validReferencedTables.map((table: any) => {
210+
const schemaName = table.schema
211+
return schemaName ? `${schemaName}.${table.name}` : table.name
212+
}))
213+
214+
console.log('[minusx] Filtering fields for', validTableNames.size, 'valid tables...')
215+
216+
const filteredFields = allFields.filter((field: any) => {
217+
const tableName = get(field, 'table_name')
218+
const tableSchema = get(field, 'schema')
219+
const fullTableName = tableSchema ? `${tableSchema}.${tableName}` : tableName
220+
221+
return validTableNames.has(tableName) || validTableNames.has(fullTableName)
222+
})
223+
224+
console.log('[minusx] Fields after filtering:', filteredFields.length, 'out of', allFields.length)
225+
226+
// Step 5: Process metadata for all three with filtered data
227+
console.log('[minusx] Processing metadata with filtered data...')
228+
229+
const [cardsHash, dbSchemaHash, fieldsHash] = await Promise.all([
230+
processMetadataWithCaching('cards', async () => cards),
231+
processMetadataWithCaching('dbSchema', async () => dbSchema),
232+
processMetadataWithCaching('fields', async () => filteredFields)
233+
])
234+
235+
console.log('[minusx] Coordinated metadata processing complete')
236+
237+
return {
238+
cardsHash,
239+
dbSchemaHash,
240+
fieldsHash
241+
}
158242
}
159243

web/src/package.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export { contains } from './helpers/utils';
1010
export { addCtesToQuery, processSQLWithCtesOrModels } from './helpers/catalogAsModels';
1111
export { GLOBAL_EVENTS, captureEvent } from './tracking'
1212
export { getParsedIframeInfo } from './helpers/origin';
13-
export { processMetadata, processCards, processDBSchema } from './helpers/metadataProcessor';
13+
export { processMetadata, processAllMetadata } from './helpers/metadataProcessor';
1414
export { dispatch } from './state/dispatch';
1515
export { updateIsDevToolsOpen, updateDevToolsTabName, addMemory } from './state/settings/reducer'
1616
export { setInstructions } from './state/thumbnails/reducer';

0 commit comments

Comments
 (0)