Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -812,4 +812,130 @@ describe('IndexPattern', () => {
).toMatchInlineSnapshot(`undefined`);
});
});

describe('Caching toSpec call', () => {
beforeEach(() => {
const formatter = {
toJSON: () => ({ id: 'bytes' }),
} as FieldFormat;
indexPattern.getFormatterForField = () => formatter;
indexPattern.getFormatterForFieldNoDefault = () => undefined;
});

test('should cache the result of toSpec', () => {
const spec1 = indexPattern.toSpec();
const spec2 = indexPattern.toSpec();
expect(spec1).toBe(spec2);
});

test('should not cache the result of toSpec when fields are not included', () => {
const spec1 = indexPattern.toSpec(false);
const spec2 = indexPattern.toSpec(false);
expect(spec1).not.toBe(spec2);
});

test('should clear the cache when fields are added', () => {
const spec1 = indexPattern.toSpec();
indexPattern.fields.add({
name: 'new_field',
type: 'keyword',
aggregatable: true,
searchable: true,
readFromDocValues: false,
});
const spec2 = indexPattern.toSpec();
expect(spec1.fields?.new_field).toBeUndefined();
expect(spec1).not.toBe(spec2);
});

test('should clear the cache when fields are removed', () => {
const spec1 = indexPattern.toSpec();
indexPattern.fields.remove(indexPattern.fields.getByName('bytes')!);
const spec2 = indexPattern.toSpec();
expect(spec1.fields?.bytes).toBeDefined();
expect(spec2.fields?.bytes).toBeUndefined();
expect(spec1).not.toBe(spec2);
});

test('should clear the cache when scripted fields are removed', () => {
indexPattern.fields.add({
name: 'new scripted field',
script: 'false',
type: 'boolean',
scripted: true,
lang: 'painless',
aggregatable: true,
searchable: true,
count: 0,
readFromDocValues: false,
});
const spec1 = indexPattern.toSpec();
indexPattern.removeScriptedField('new scripted field');
const spec2 = indexPattern.toSpec();
expect(spec1).not.toBe(spec2);
});

test('should clear the cache when runtime fields are added', () => {
const spec1 = indexPattern.toSpec();
indexPattern.addRuntimeField('new_runtime_field', runtimeFieldScript);
const spec2 = indexPattern.toSpec();
expect(spec1).not.toBe(spec2);
});

test('should clear the cache when runtime fields are removed', () => {
indexPattern.addRuntimeField('new_runtime_field', runtimeFieldScript);
const spec1 = indexPattern.toSpec();
indexPattern.removeRuntimeField('new_runtime_field');
const spec2 = indexPattern.toSpec();
expect(spec1).not.toBe(spec2);
});

test('should clear the cache when field custom label is changed', () => {
const spec1 = indexPattern.toSpec();
indexPattern.setFieldCustomLabel('bytes', 'new label');
const spec2 = indexPattern.toSpec();
expect(spec1).not.toBe(spec2);
});

test('should clear the cache when field custom description is changed', () => {
const spec1 = indexPattern.toSpec();
indexPattern.setFieldCustomDescription('bytes', 'new description');
const spec2 = indexPattern.toSpec();
expect(spec1).not.toBe(spec2);
});

test('should clear the cache when field count is changed', () => {
const spec1 = indexPattern.toSpec();
indexPattern.setFieldCount('bytes', 123);
const spec2 = indexPattern.toSpec();
expect(spec1).not.toBe(spec2);
});

test('should clear the cache when composite runtime field is added', () => {
const spec1 = indexPattern.toSpec();
indexPattern.addRuntimeField('new_runtime_field', {
type: 'composite',
script: {
source: "emit('hello world');",
},
fields: {
a: {
type: 'keyword',
},
b: {
type: 'long',
},
},
});
const spec2 = indexPattern.toSpec();
expect(spec1).not.toBe(spec2);
});

test('should clear the cache when index pattern title is changed', () => {
const spec1 = indexPattern.toSpec();
indexPattern.setIndexPattern('new title');
const spec2 = indexPattern.toSpec();
expect(spec1).not.toBe(spec2);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { castEsToKbnFieldTypeName } from '@kbn/field-types';
import { CharacterNotAllowedInField } from '@kbn/kibana-utils-plugin/common';
import type { DataViewBase } from '@kbn/es-query';
import { each, mapValues, pick, pickBy, reject } from 'lodash';
import deepEqual from 'fast-deep-equal';
import type { DataViewField, IIndexPatternFieldList } from '../fields';
import { fieldList } from '../fields';
import type {
Expand All @@ -23,6 +24,7 @@ import type {
RuntimeFieldSpec,
RuntimeType,
FieldSpec,
DataViewMinimalSpec,
} from '../types';
import { removeFieldAttrs } from './utils';
import { AbstractDataView } from './abstract_data_views';
Expand Down Expand Up @@ -64,6 +66,9 @@ export class DataView extends AbstractDataView implements DataViewBase {

private etag: string | undefined;

private dataViewSpecCache: DataViewSpec | null = null;
private cachedMinimalSpec: DataViewMinimalSpec | null = null;

/**
* constructor
* @param config - config data and dependencies
Expand Down Expand Up @@ -139,12 +144,37 @@ export class DataView extends AbstractDataView implements DataViewBase {
};
}

/**
* Check if we should skip using the cached data view spec
* @returns true if we should skip using the cached spec
*/
private shouldSkipCache(includeFields: boolean) {
if (!this.dataViewSpecCache) return true;
if (!includeFields) return true;
if (this.cachedMinimalSpec && !deepEqual(this.cachedMinimalSpec, this.toMinimalSpec())) {
return true;
}

// Check if any fields were added or removed
const cachedFields = this.dataViewSpecCache.fields ?? {};
const cachedFieldsKeys = Object.keys(cachedFields);

if (cachedFieldsKeys.length !== this.fields.length) {
return true;
}

return false;
}

/**
* Creates static representation of the data view.
* @param includeFields Whether or not to include the `fields` list as part of this spec. If not included, the list
* will be fetched from Elasticsearch when instantiating a new Data View with this spec.
*/
public toSpec(includeFields = true): DataViewSpec {
if (this.dataViewSpecCache && !this.shouldSkipCache(includeFields)) {
return this.dataViewSpecCache;
}
const spec = this.toSpecShared(includeFields);
const fields =
includeFields && this.fields
Expand All @@ -155,15 +185,27 @@ export class DataView extends AbstractDataView implements DataViewBase {
spec.fields = fields;
}

if (includeFields) {
this.dataViewSpecCache = spec;
this.cachedMinimalSpec = this.toMinimalSpec();
}

return spec;
}

/**
* Clear instance in data view spec cache
*/
clearDataViewSpecCache = () => {
this.dataViewSpecCache = null;
this.cachedMinimalSpec = null;
};
/**
* Creates a minimal static representation of the data view. Fields and popularity scores will be omitted.
*/
public toMinimalSpec(params?: {
keepFieldAttrs?: Array<'customLabel' | 'customDescription'>;
}): Omit<DataViewSpec, 'fields'> {
}): DataViewMinimalSpec {
const fieldAttrsToKeep = params?.keepFieldAttrs ?? ['customLabel', 'customDescription'];

// removes `fields`
Expand Down Expand Up @@ -194,6 +236,7 @@ export class DataView extends AbstractDataView implements DataViewBase {
*/

removeScriptedField(fieldName: string) {
this.clearDataViewSpecCache();
this.deleteScriptedFieldInternal(fieldName);
const field = this.fields.getByName(fieldName);
if (field && field.scripted) {
Expand Down Expand Up @@ -272,6 +315,7 @@ export class DataView extends AbstractDataView implements DataViewBase {
throw new CharacterNotAllowedInField('*', name);
}

this.clearDataViewSpecCache();
const { type, script, customLabel, customDescription, format, popularity } = runtimeField;

if (type === 'composite') {
Expand Down Expand Up @@ -335,6 +379,7 @@ export class DataView extends AbstractDataView implements DataViewBase {
* @param name - Field name to remove
*/
removeRuntimeField(name: string) {
this.clearDataViewSpecCache();
const existingField = this.getFieldByName(name);

if (existingField && existingField.isMapped) {
Expand Down Expand Up @@ -374,6 +419,7 @@ export class DataView extends AbstractDataView implements DataViewBase {
*/

public setFieldCustomLabel(fieldName: string, customLabel: string | undefined | null) {
this.clearDataViewSpecCache();
const fieldObject = this.fields.getByName(fieldName);
const newCustomLabel: string | undefined = customLabel === null ? undefined : customLabel;

Expand All @@ -394,6 +440,7 @@ export class DataView extends AbstractDataView implements DataViewBase {
fieldName: string,
customDescription: string | undefined | null
) {
this.clearDataViewSpecCache();
const fieldObject = this.fields.getByName(fieldName);
const newCustomDescription: string | undefined =
customDescription === null ? undefined : customDescription;
Expand All @@ -412,6 +459,7 @@ export class DataView extends AbstractDataView implements DataViewBase {
*/

public setFieldCount(fieldName: string, count: number | undefined | null) {
this.clearDataViewSpecCache();
const fieldObject = this.fields.getByName(fieldName);
const newCount: number | undefined = count === null ? undefined : count;

Expand Down Expand Up @@ -444,6 +492,7 @@ export class DataView extends AbstractDataView implements DataViewBase {
);
}

this.clearDataViewSpecCache();
// We first remove the runtime composite field with the same name which will remove all of its subFields.
// This guarantees that we don't leave behind orphan data view fields
this.removeRuntimeField(name);
Expand Down Expand Up @@ -479,6 +528,7 @@ export class DataView extends AbstractDataView implements DataViewBase {
);
}

this.clearDataViewSpecCache();
// Create the field if it does not exist or update an existing one
let createdField: DataViewField | undefined;
const existingField = this.getFieldByName(fieldName);
Expand Down Expand Up @@ -517,6 +567,7 @@ export class DataView extends AbstractDataView implements DataViewBase {

upsertScriptedField = (field: FieldSpec) => {
this.upsertScriptedFieldInternal(field);
this.clearDataViewSpecCache();
const fieldExists = !!this.fields.getByName(field.name);

if (fieldExists) {
Expand Down
2 changes: 2 additions & 0 deletions src/platform/plugins/shared/data_views/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,8 @@ export type DataViewSpec = {
allowHidden?: boolean;
};

export type DataViewMinimalSpec = Omit<DataViewSpec, 'fields'>;

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type SourceFilter = {
value: string;
Expand Down