Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
*/

import { DataView } from '@kbn/data-views-plugin/common';
import { existingFields, buildFieldList } from './field_existing_utils';
import type { DataViewField, DataViewsContract } from '@kbn/data-views-plugin/common';
import { getExistingFields, buildFieldList, fetchFieldExistence } from './field_existing_utils';

describe('existingFields', () => {
it('should remove missing fields by matching names', () => {
expect(
existingFields(
getExistingFields(
[
{ name: 'a', aggregatable: true, searchable: true, type: 'string' },
{ name: 'b', aggregatable: true, searchable: true, type: 'string' },
Expand All @@ -29,7 +30,7 @@ describe('existingFields', () => {

it('should keep scripted and runtime fields', () => {
expect(
existingFields(
getExistingFields(
[{ name: 'a', aggregatable: true, searchable: true, type: 'string' }],
[
{ name: 'a', isScript: false, isMeta: false },
Expand Down Expand Up @@ -88,3 +89,119 @@ describe('buildFieldList', () => {
});
});
});

describe('fetchFieldExistence', () => {
const mockDslQuery = { match_all: {} };
const mockMetaFields = ['_id', '_type'];

const createMockDataView = (fields: Array<Partial<DataViewField>> = []) => {
return {
getIndexPattern: jest.fn().mockReturnValue('test-pattern'),
getFieldByName: jest.fn((name) => fields.find((f) => f.name === name)),
fields,
} as unknown as DataView;
};

const createMockDataViewsService = (fields: Array<Partial<DataViewField>>) => {
return {
getFieldsForIndexPattern: jest.fn().mockResolvedValue(fields),
refreshFields: jest.fn().mockResolvedValue(undefined),
} as unknown as DataViewsContract;
};

const mockSearch = jest.fn().mockResolvedValue({});

it.each([
{
scenario: 'field changes from unmapped to mapped',
shouldRefresh: true,
existingFields: [
{
name: 'previously_unmapped_field',
type: 'text',
isMapped: false,
},
],
returnedFields: [
{
name: 'previously_unmapped_field',
type: 'keyword',
aggregatable: true,
searchable: true,
},
],
},
{
scenario: 'field type changes',
shouldRefresh: true,
existingFields: [
{
name: 'changing_type_field',
type: 'keyword',
isMapped: true,
},
],
returnedFields: [
{
name: 'changing_type_field',
type: 'long',
aggregatable: true,
searchable: true,
},
],
},
{
scenario: 'fields are unchanged',
shouldRefresh: false,
existingFields: [
{
name: 'unchanged_field',
type: 'keyword',
isMapped: true,
},
],
returnedFields: [
{
name: 'unchanged_field',
type: 'keyword',
aggregatable: true,
searchable: true,
},
],
},
{
scenario: 'new fields are detected',
shouldRefresh: true,
existingFields: [
{
name: 'existing_field',
type: 'keyword',
isMapped: true,
},
// 'new_field' is deliberately missing to simulate a new field
],
returnedFields: [
{ name: 'existing_field', type: 'keyword', aggregatable: true, searchable: true },
{ name: 'new_field', type: 'long', aggregatable: true, searchable: true },
],
},
])('should handle when $scenario', async ({ shouldRefresh, existingFields, returnedFields }) => {
const mockDataViewsService = createMockDataViewsService(returnedFields);
const mockDataView = createMockDataView(existingFields as Array<Partial<DataViewField>>);

await fetchFieldExistence({
search: mockSearch,
dataViewsService: mockDataViewsService,
dataView: mockDataView,
dslQuery: mockDslQuery,
includeFrozen: false,
metaFields: mockMetaFields,
});

if (shouldRefresh) {
expect(mockDataViewsService.refreshFields).toHaveBeenCalledWith(mockDataView, false, true);
} else {
expect(mockDataViewsService.refreshFields).not.toHaveBeenCalled();
}
Comment on lines +201 to +205
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: should we split the tests in the ones that refresh and the ones that don't so we keep them without any logic?

});
});
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,32 @@ export async function fetchFieldExistence({
includeEmptyFields: false,
});

// take care of fields of existingFieldList, that are not yet available
// in the given data view. Those fields we consider as new fields,
// that were ingested after the data view was loaded
const newFields = existingFieldList.filter((field) => !dataView.getFieldByName(field.name));
// refresh the data view in case there are new fields
if (newFields.length) {
// Identify two categories of fields requiring a refresh:
// 1. New fields - fields that don't exist in the current data view
// 2. Fields with mapping changes - existing fields with updated mapping information
const newFields: typeof existingFieldList = [];
const fieldsWithMappingChanges: typeof existingFieldList = [];

for (const field of existingFieldList) {
const previousField = dataView.getFieldByName(field.name);
if (!previousField) {
newFields.push(field);
} else if (previousField.type !== field.type) {
fieldsWithMappingChanges.push(field);
}
}

// Refresh if either new fields or mapping changes are detected
const needsRefresh = newFields.length > 0 || fieldsWithMappingChanges.length > 0;

if (needsRefresh) {
// Refresh with force=true to ensure all fields are properly loaded
await dataViewsService.refreshFields(dataView, false, true);
}
const allFields = buildFieldList(dataView, metaFields);
return {
indexPatternTitle: dataView.getIndexPattern(),
existingFieldNames: existingFields(existingFieldList, allFields),
existingFieldNames: getExistingFields(existingFieldList, allFields),
newFields,
};
}
Expand Down Expand Up @@ -126,7 +140,7 @@ function toQuery(
/**
* Exported only for unit tests.
*/
export function existingFields(filteredFields: FieldSpec[], allFields: Field[]): string[] {
export function getExistingFields(filteredFields: FieldSpec[], allFields: Field[]): string[] {
const filteredFieldsSet = new Set(filteredFields.map((f) => f.name));

return allFields
Expand Down