From f593f90413c47b9077699aac7296f9b0f67a173f Mon Sep 17 00:00:00 2001 From: ryjiang Date: Mon, 23 Dec 2024 14:12:12 +0800 Subject: [PATCH] [2.4] support expr template values for search/query/delete (#391) * add formatExprValues formatter Signed-off-by: ryjiang * finish Signed-off-by: ryjiang --------- Signed-off-by: ryjiang --- milvus/grpc/Data.ts | 23 +++++- milvus/types/Common.ts | 5 +- milvus/types/Data.ts | 20 +++-- milvus/utils/Format.ts | 134 ++++++++++++++++++++++++++---- test/grpc/Data.spec.ts | 132 +++++++++++++++++++++++++++--- test/tools/collection.ts | 4 +- test/tools/const.ts | 2 +- test/tools/data.ts | 12 +-- test/utils/Format.spec.ts | 166 +++++++++++++++++++++++++++++++++++++- 9 files changed, 451 insertions(+), 47 deletions(-) diff --git a/milvus/grpc/Data.ts b/milvus/grpc/Data.ts index 8aa828a0..219254f0 100644 --- a/milvus/grpc/Data.ts +++ b/milvus/grpc/Data.ts @@ -67,6 +67,7 @@ import { f32ArrayToBinaryBytes, NO_LIMIT, DescribeCollectionReq, + formatExprValues, } from '../'; import { Collection } from './Collection'; @@ -345,10 +346,17 @@ export class Data extends Collection { // filter > expr data.expr = data.filter || data.expr; + const req = data as any; + + // if exprValues exist, format it + if (data.exprValues) { + req.expr_template_values = formatExprValues(data.exprValues); + } + const promise = await promisify( this.channelPool, 'Delete', - data, + req, data.timeout || this.timeout ); return promise; @@ -410,7 +418,7 @@ export class Data extends Collection { if ((data as DeleteByFilterReq).filter) { expr = (data as DeleteByFilterReq).filter; } - const req = { ...data, expr }; + const req = { ...data, expr } as any; return this.deleteEntities(req); } @@ -459,7 +467,9 @@ export class Data extends Collection { describeCollectionRequest.db_name = data.db_name; } - const collectionInfo = await this.describeCollection(describeCollectionRequest); + const collectionInfo = await this.describeCollection( + describeCollectionRequest + ); // build search params const { request, nq, round_decimal, isHybridSearch } = buildSearchRequest( @@ -755,7 +765,7 @@ export class Data extends Collection { }); // search data - const res = await client.query(data); + const res = await client.query(data as QueryReq); // get last item of the data const lastItem = res.data[res.data.length - 1]; @@ -926,6 +936,11 @@ export class Data extends Collection { // filter > expr or empty > ids data.expr = data.filter || data.expr || primaryKeyInIdsExpression; + // if exprValues exist, format it + if (data.exprValues) { + (data as any).expr_template_values = formatExprValues(data.exprValues); + } + // Execute the query and get the results const promise: QueryRes = await promisify( this.channelPool, diff --git a/milvus/types/Common.ts b/milvus/types/Common.ts index 05f8a7db..5d575ecb 100644 --- a/milvus/types/Common.ts +++ b/milvus/types/Common.ts @@ -58,7 +58,10 @@ export interface TimeStampArray { created_utc_timestamps: string[]; } -export type keyValueObj = Record; +export type keyValueObj = Record< + string, + string | number | string[] | number[] | unknown +>; export interface collectionNameReq extends GrpcTimeOut { collection_name: string; // required, collection name diff --git a/milvus/types/Data.ts b/milvus/types/Data.ts index e32ffb1c..dab6e930 100644 --- a/milvus/types/Data.ts +++ b/milvus/types/Data.ts @@ -120,12 +120,11 @@ interface BaseDeleteReq extends collectionNameReq { | 'Bounded' | 'Eventually' | 'Customized'; // consistency level + exprValues?: keyValueObj; // template values for filter expression, eg: {key: 'value'} } -export interface DeleteEntitiesReq extends BaseDeleteReq { - filter?: string; // filter expression - expr?: string; // alias for filter -} +export type DeleteEntitiesReq = BaseDeleteReq & + ({ expr?: string; filter?: never } | { filter?: string; expr?: never }); export interface DeleteByIdsReq extends BaseDeleteReq { ids: string[] | number[]; // primary key values @@ -288,6 +287,7 @@ export interface SearchReq extends collectionNameReq { anns_field?: string; // your vector field name partition_names?: string[]; // partition names expr?: string; // filter expression + exprValues?: keyValueObj; // template values for filter expression, eg: {key: 'value'} search_params: SearchParam; // search parameters vectors: VectorTypes[]; // vectors to search output_fields?: string[]; // fields to return @@ -321,6 +321,7 @@ export interface SearchSimpleReq extends collectionNameReq { offset?: number; // skip how many results filter?: string; // filter expression expr?: string; // alias for filter + exprValues?: keyValueObj; // template values for filter expression, eg: {key: 'value'} params?: keyValueObj; // extra search parameters metric_type?: string; // distance metric type consistency_level?: ConsistencyLevelEnum; // consistency level @@ -336,6 +337,7 @@ export type HybridSearchSingleReq = Pick< > & { data: VectorTypes[] | VectorTypes; // vector to search expr?: string; // filter expression + exprValues?: keyValueObj; // template values for filter expression, eg: {key: 'value'} params?: keyValueObj; // extra search parameters transformers?: OutputTransformers; // provide custom data transformer for specific data type like bf16 or f16 vectors }; @@ -406,17 +408,21 @@ export type OutputTransformers = { [DataType.SparseFloatVector]?: (sparse: SparseVectorDic) => SparseFloatVector; }; -export interface QueryReq extends collectionNameReq { +type BaseQueryReq = collectionNameReq & { output_fields?: string[]; // fields to return partition_names?: string[]; // partition names ids?: string[] | number[]; // primary key values - expr?: string; // filter expression + expr?: string; // filter expression, or template string, eg: "key = {key}" filter?: string; // alias for expr offset?: number; // skip how many results limit?: number; // how many results you want consistency_level?: ConsistencyLevelEnum; // consistency level transformers?: OutputTransformers; // provide custom data transformer for specific data type like bf16 or f16 vectors -} + exprValues?: keyValueObj; // template values for filter expression, eg: {key: 'value'} +}; + +export type QueryReq = BaseQueryReq & + ({ expr?: string; filter?: never } | { filter?: string; expr?: never }); export interface QueryIteratorReq extends Omit { diff --git a/milvus/utils/Format.ts b/milvus/utils/Format.ts index b4fb3247..9fa0e0f1 100644 --- a/milvus/utils/Format.ts +++ b/milvus/utils/Format.ts @@ -41,6 +41,7 @@ import { f32ArrayToF16Bytes, bf16BytesToF32Array, f16BytesToF32Array, + keyValueObj, } from '../'; /** @@ -643,6 +644,21 @@ export const convertRerankParams = (rerank: RerankerObj) => { return r; }; +type FormatedSearchRequest = { + collection_name: string; + partition_names: string[]; + output_fields: string[]; + nq?: number; + dsl?: string; + dsl_type?: DslType; + placeholder_group?: Uint8Array; + search_params?: KeyValuePair[]; + consistency_level: ConsistencyLevelEnum; + expr?: string; + expr_template_values?: keyValueObj; + rank_params?: KeyValuePair[]; +}; + /** * This method is used to build search request for a given data. * It first fetches the collection info and then constructs the search request based on the data type. @@ -676,17 +692,7 @@ export const buildSearchRequest = ( const searchSimpleReq = data as SearchSimpleReq; // Initialize requests array - const requests: { - collection_name: string; - partition_names: string[]; - output_fields: string[]; - nq: number; - dsl: string; - dsl_type: DslType; - placeholder_group: Uint8Array; - search_params: KeyValuePair[]; - consistency_level: ConsistencyLevelEnum; - }[] = []; + const requests: FormatedSearchRequest[] = []; // detect if the request is hybrid search request const isHybridSearch = !!( @@ -739,12 +745,12 @@ export const buildSearchRequest = ( searchingVector = formatSearchVector(searchingVector, field.dataType!); // create search request - requests.push({ + const request: FormatedSearchRequest = { collection_name: req.collection_name, partition_names: req.partition_names || [], output_fields: req.output_fields || default_output_fields, nq: searchReq.nq || searchingVector.length, - dsl: searchReq.expr || searchSimpleReq.filter || '', + dsl: req.expr || searchReq.expr || searchSimpleReq.filter || '', // expr dsl_type: DslType.BoolExprV1, placeholder_group: buildPlaceholderGroupBytes( milvusProto, @@ -756,7 +762,14 @@ export const buildSearchRequest = ( ), consistency_level: req.consistency_level || (collectionInfo.consistency_level as any), - }); + }; + + // if exprValues is set, add it to the request(inner) + if (req.exprValues) { + request.expr_template_values = formatExprValues(req.exprValues); + } + + requests.push(request); } } @@ -774,7 +787,7 @@ export const buildSearchRequest = ( return { isHybridSearch, request: isHybridSearch - ? { + ? ({ collection_name: data.collection_name, partition_names: data.partition_names, requests: requests, @@ -791,7 +804,7 @@ export const buildSearchRequest = ( ], output_fields: requests[0]?.output_fields, consistency_level: requests[0]?.consistency_level, - } + } as FormatedSearchRequest) : requests[0], nq: requests[0].nq, round_decimal, @@ -910,3 +923,92 @@ export const formatSearchVector = ( return searchVector as VectorTypes[]; } }; + +type TemplateValue = + | { bool_val: boolean } + | { int64_val: number } + | { float_val: number } + | { string_val: string } + | { array_val: TemplateArrayValue }; +type TemplateArrayValue = + | { bool_data: { data: boolean[] } } + | { long_data: { data: number[] } } + | { double_data: { data: number[] } } + | { string_data: { data: string[] } } + | { json_data: { data: any[] } } + | { array_data: { data: TemplateArrayValue[] } }; + +export const formatExprValues = ( + exprValues: Record +): Record => { + const result: Record = {}; + for (const [key, value] of Object.entries(exprValues)) { + if (Array.isArray(value)) { + // Handle arrays + result[key] = { array_val: convertArray(value) }; + } else { + // Handle primitive types + if (typeof value === 'boolean') { + result[key] = { bool_val: value }; + } else if (typeof value === 'number') { + result[key] = Number.isInteger(value) + ? { int64_val: value } + : { float_val: value }; + } else if (typeof value === 'string') { + result[key] = { string_val: value }; + } + } + } + return result; +}; +const convertArray = (arr: any[]): TemplateArrayValue => { + const first = arr[0]; + switch (typeof first) { + case 'boolean': + return { + bool_data: { + data: arr, + }, + }; + case 'number': + if (Number.isInteger(first)) { + return { + long_data: { + data: arr, + }, + }; + } else { + return { + double_data: { + data: arr, + }, + }; + } + case 'string': + return { + string_data: { + data: arr, + }, + }; + case 'object': + if (Array.isArray(first)) { + return { + array_data: { + data: arr.map(convertArray), + }, + }; + } else { + return { + json_data: { + data: arr, + }, + }; + } + default: + return { + string_data: { + data: arr, + }, + }; + } +}; diff --git a/test/grpc/Data.spec.ts b/test/grpc/Data.spec.ts index ce63aeda..acd3c9fc 100644 --- a/test/grpc/Data.spec.ts +++ b/test/grpc/Data.spec.ts @@ -13,12 +13,13 @@ import { genCollectionParams, VECTOR_FIELD_NAME, GENERATE_NAME, - // DEFAULT_VALUE, + DEFAULT_NUM_VALUE, } from '../tools'; import { timeoutTest } from '../tools'; const milvusClient = new MilvusClient({ address: IP, + logLevel: 'info', }); const COLLECTION_NAME = GENERATE_NAME(); const VARCHAR_ID_COLLECTION_NAME = GENERATE_NAME(); @@ -30,6 +31,14 @@ const createCollectionParams = genCollectionParams({ dim: [4], vectorType: [DataType.FloatVector], autoID: false, + fields: [ + { + name: 'varChar2', + description: 'VarChar2 field', + data_type: DataType.VarChar, + max_length: 100, + }, + ], }); const createCollectionParamsVarcharID = genCollectionParams({ collectionName: VARCHAR_ID_COLLECTION_NAME, @@ -61,6 +70,7 @@ describe(`Data.API`, () => { collection_name: COLLECTION_NAME, data: generateInsertData(createCollectionParams.fields, 1024), }); + await milvusClient.insert({ collection_name: VARCHAR_ID_COLLECTION_NAME, data: generateInsertData(createCollectionParamsVarcharID.fields, 1024), @@ -100,6 +110,16 @@ describe(`Data.API`, () => { }); afterAll(async () => { + const searchParams = { + collection_name: COLLECTION_NAME, + // partition_names: [], + filter: 'json["number"] >= 0', + data: [1, 2, 3, 4], + limit: 4, + output_fields: ['id', 'json'], + }; + const res = await milvusClient.search(searchParams); + await milvusClient.dropCollection({ collection_name: COLLECTION_NAME, }); @@ -231,13 +251,21 @@ describe(`Data.API`, () => { it(`Exec simple search without params and output fields should success`, async () => { const limit = 4; - // collection search + const describe = await milvusClient.describeCollection({ + collection_name: COLLECTION_NAME, + }); + + // find varchar2 field + describe.schema.fields.find(f => f.name === 'varChar2'); + + // console.dir(varChar2Field, { depth: null }); + const searchWithData = await milvusClient.search({ collection_name: COLLECTION_NAME, filter: '', - data: [1, 2, 3, 4], + vector: [1, 2, 3, 4], limit: limit, - group_by_field: 'varChar', + group_by_field: 'varChar2', }); expect(searchWithData.status.error_code).toEqual(ErrorCode.SUCCESS); @@ -319,6 +347,20 @@ describe(`Data.API`, () => { expect(Number(r.int64)).toBeLessThan(10000); }); + const resExprValues = await milvusClient.search({ + collection_name: COLLECTION_NAME, + filter: 'int64 < {value}', + exprValues: { value: 10000 }, + data: [1, 2, 3, 4], + limit: limit, + params: { nprobe: 1024 }, + }); + + expect(resExprValues.status.error_code).toEqual(ErrorCode.SUCCESS); + resExprValues.results.forEach(r => { + expect(Number(r.int64)).toBeLessThan(10000); + }); + const res2 = await milvusClient.search({ collection_name: COLLECTION_NAME, expr: 'int64 < 10000', @@ -488,16 +530,47 @@ describe(`Data.API`, () => { it(`Query with data limit and offset`, async () => { const res = await milvusClient.query({ collection_name: COLLECTION_NAME, - expr: 'id > 0', - output_fields: ['id', VECTOR_FIELD_NAME, 'default_value'], + expr: `id > 0 and default_value != ${DEFAULT_NUM_VALUE}`, + output_fields: ['id', VECTOR_FIELD_NAME, 'default_value', 'int32_array'], offset: 0, limit: 3, }); - // res.data.forEach(d => { - // expect(d.default_value).toEqual(DEFAULT_VALUE); - // }); expect(res.data.length).toBe(3); + + const res2 = await milvusClient.query({ + collection_name: COLLECTION_NAME, + expr: 'id > {value}', + output_fields: ['id', VECTOR_FIELD_NAME, 'default_value'], + offset: 0, + limit: 3, + exprValues: { value: 0 }, + }); + + expect(res2.data.length).toBe(3); + + // get all default values + const default_values = res.data.map(d => d.default_value); + + console.log('default_values', default_values); + + // query by ids + const res3 = await milvusClient.query({ + collection_name: COLLECTION_NAME, + expr: `default_value in [${default_values.join(',')}]`, + output_fields: ['default_value'], + }); + + expect(res3.data.length).toBe(default_values.length); + + const res4 = await milvusClient.query({ + collection_name: COLLECTION_NAME, + expr: 'default_value in {default_values}', + output_fields: ['default_value'], + exprValues: { default_values }, + }); + + expect(res4.data.length).toBe(default_values.length); }); it(`Query with count(*)`, async () => { @@ -557,6 +630,17 @@ describe(`Data.API`, () => { limit: 3, }); expect(res.data.length).toBe(3); + + const template = 'json["number"] >= {value}'; + const res2 = await milvusClient.query({ + collection_name: COLLECTION_NAME, + expr: template, + output_fields: ['id', 'json', VECTOR_FIELD_NAME], + offset: 0, + limit: 3, + exprValues: { value: 1.0 }, + }); + expect(res2.data.length).toBe(3); }); it(`Query with data without limit and offset`, async () => { @@ -586,6 +670,7 @@ describe(`Data.API`, () => { expect(res.data.length).toEqual(0); }); + let default_values: string[] = []; it(`query by ids success`, async () => { const query = await milvusClient.query({ collection_name: COLLECTION_NAME, @@ -598,10 +683,12 @@ describe(`Data.API`, () => { const query0 = await milvusClient.query({ collection_name: VARCHAR_ID_COLLECTION_NAME, expr: 'id != ""', + output_fields: ['id', 'default_value'], }); // get first 3 ids const ids = query0.data.slice(0, 3).map(d => d.id); + default_values = query0.data.slice(0, 3).map(d => d.default_value); // query by ids const queryVarcharIds = await milvusClient.query({ collection_name: VARCHAR_ID_COLLECTION_NAME, @@ -611,6 +698,25 @@ describe(`Data.API`, () => { expect(queryVarcharIds.data.length).toEqual(3); }); + it(`delete entities with exprValues should success`, async () => { + const res = await milvusClient.deleteEntities({ + collection_name: COLLECTION_NAME, + expr: 'default_value in {value}', + exprValues: { value: default_values }, + }); + + expect(res.status.error_code).toEqual(ErrorCode.SUCCESS); + + // query again + const query = await milvusClient.query({ + collection_name: COLLECTION_NAME, + expr: 'default_value in {value}', + exprValues: { value: default_values }, + }); + + expect(query.data.length).toEqual(0); + }); + it('delete withouth colleciton name should throw error', async () => { try { await milvusClient.deleteEntities({ @@ -644,6 +750,14 @@ describe(`Data.API`, () => { }); expect(res2.status.error_code).toEqual(ErrorCode.SUCCESS); + + // query again + const query2 = await milvusClient.query({ + collection_name: VARCHAR_ID_COLLECTION_NAME, + expr: 'id != ""', + }); + + expect(query2.data.length).toEqual(0); }); it(`delete by filter should success`, async () => { diff --git a/test/tools/collection.ts b/test/tools/collection.ts index e1f83851..122c672c 100644 --- a/test/tools/collection.ts +++ b/test/tools/collection.ts @@ -108,8 +108,8 @@ export const genCollectionParams = (data: { { name: 'default_value', // default_value: DEFAULT_VALUE, - description: 'int64 field', - data_type: 'Int64', // test string data type + description: 'int32 field', + data_type: 'Int32', // test string data type }, { name: 'varChar', diff --git a/test/tools/const.ts b/test/tools/const.ts index a63c2adb..ec2148b3 100644 --- a/test/tools/const.ts +++ b/test/tools/const.ts @@ -3,7 +3,7 @@ export const INDEX_NAME = 'index_name'; export const DIMENSION = 4; export const INDEX_FILE_SIZE = 1024; export const PARTITION_TAG = 'random'; -export const DEFAULT_VALUE = '100'; +export const DEFAULT_NUM_VALUE = 100; export const MAX_LENGTH = 8; export const MAX_CAPACITY = 4; export const VECTOR_FIELD_NAME = 'vector'; diff --git a/test/tools/data.ts b/test/tools/data.ts index f1c4dfb2..7e56f956 100644 --- a/test/tools/data.ts +++ b/test/tools/data.ts @@ -5,7 +5,7 @@ import { FieldType, SparseVectorCOO, } from '../../milvus'; -import { MAX_LENGTH, P_KEY_VALUES } from './const'; +import { MAX_LENGTH, P_KEY_VALUES, DEFAULT_NUM_VALUE } from './const'; import Long from 'long'; interface DataGenerator { @@ -282,17 +282,17 @@ export const generateInsertData = ( const value: { [x: string]: FieldData } = {}; // Initialize an empty object to store the generated values for this data point for (const field of fields) { - // Skip autoID and fields with default values - if (field.autoID || typeof field.default_value !== 'undefined') { + // Skip autoID and fields + if (field.autoID) { continue; } // get data type const data_type = convertToDataType(field.data_type); - // Skip fields with default values - if (typeof field.default_value !== 'undefined') { - continue; + // if field name is default_value, insert 50% of data with default value + if (field.name === 'default_value' && Math.random() > 0.5) { + value[field.name] = DEFAULT_NUM_VALUE; } // Parameters used to generate all types of data diff --git a/test/utils/Format.spec.ts b/test/utils/Format.spec.ts index 8317f2b7..84b77077 100644 --- a/test/utils/Format.spec.ts +++ b/test/utils/Format.spec.ts @@ -28,6 +28,7 @@ import { Field, formatSearchVector, buildSearchRequest, + formatExprValues, } from '../../milvus'; describe('utils/format', () => { @@ -705,7 +706,8 @@ describe('utils/format', () => { [1, 2, 3], [4, 5, 6], ], - expr: 'id > 0', + expr: 'id > {value}', + exprValues: { value: 1 }, output_fields: ['*'], }; @@ -763,6 +765,10 @@ describe('utils/format', () => { expect(searchRequest.request.collection_name).toEqual('test'); expect(searchRequest.request.output_fields).toEqual(['*']); expect(searchRequest.request.consistency_level).toEqual('Session'); + expect(searchRequest.request.dsl).toEqual('id > {value}'); + expect(searchRequest.request.expr_template_values).toEqual( + formatExprValues({ value: 1 }) + ); expect(searchRequest.nq).toEqual(2); const searchParamsKeyValuePairArray = (searchRequest.request as any) .search_params; @@ -799,10 +805,13 @@ describe('utils/format', () => { data: [1, 2, 3, 4, 5, 6, 7, 8], anns_field: 'vector', params: { nprobe: 2 }, + expr: 'id > 0', }, { data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], anns_field: 'vector1', + expr: 'id > {value}', + exprValues: { value: 1 }, }, ], limit: 2, @@ -894,12 +903,167 @@ describe('utils/format', () => { expect(searchParamsKeyValuePairObject.anns_field).toEqual('vector'); expect(searchParamsKeyValuePairObject.params).toEqual('{"nprobe":2}'); expect(searchParamsKeyValuePairObject.topk).toEqual(2); + expect(request.dsl).toEqual('id > 0'); } else { expect(searchParamsKeyValuePairObject.anns_field).toEqual('vector1'); expect(searchParamsKeyValuePairObject.params).toEqual('{}'); expect(searchParamsKeyValuePairObject.topk).toEqual(2); + expect(request.dsl).toEqual('id > {value}'); + expect(request.expr_template_values).toEqual( + formatExprValues({ value: 1 }) + ); } } ); }); + + it('should format exprValues correctly', () => { + const exprValues = { + bool: true, + number: 25, + float: 5.9, + string: 'Alice', + strArr: ['developer', 'javascript'], + boolArr: [true, false], + numberArr: [1, 2, 3, 4], + doubleArr: [1.1, 2.2, 3.3], + jsonArr: [{ key: 'value' }, { key: 'value' }], + intArrArr: [ + [1, 2], + [3, 4], + ], + doubleArrArr: [ + [1.1, 2.2], + [3.3, 4.4], + ], + boolArrArr: [ + [true, false], + [false, true], + ], + strArrArr: [ + ['a', 'b'], + ['c', 'd'], + ], + intArrArrArr: [ + [ + [1, 2], + [3, 4], + ], + [ + [5, 6], + [7, 8], + ], + ], + defaultArr: [undefined, undefined], + }; + const formattedExprValues = formatExprValues(exprValues); + expect(formattedExprValues).toEqual({ + bool: { bool_val: true }, + number: { int64_val: 25 }, + float: { float_val: 5.9 }, + string: { string_val: 'Alice' }, + strArr: { + array_val: { string_data: { data: ['developer', 'javascript'] } }, + }, + boolArr: { + array_val: { bool_data: { data: [true, false] } }, + }, + numberArr: { + array_val: { long_data: { data: [1, 2, 3, 4] } }, + }, + doubleArr: { + array_val: { double_data: { data: [1.1, 2.2, 3.3] } }, + }, + jsonArr: { + array_val: { + json_data: { data: [{ key: 'value' }, { key: 'value' }] }, + }, + }, + intArrArr: { + array_val: { + array_data: { + data: [ + { + long_data: { data: [1, 2] }, + }, + { + long_data: { data: [3, 4] }, + }, + ], + }, + }, + }, + doubleArrArr: { + array_val: { + array_data: { + data: [ + { + double_data: { data: [1.1, 2.2] }, + }, + { + double_data: { data: [3.3, 4.4] }, + }, + ], + }, + }, + }, + boolArrArr: { + array_val: { + array_data: { + data: [ + { + bool_data: { data: [true, false] }, + }, + { + bool_data: { data: [false, true] }, + }, + ], + }, + }, + }, + strArrArr: { + array_val: { + array_data: { + data: [ + { + string_data: { data: ['a', 'b'] }, + }, + { + string_data: { data: ['c', 'd'] }, + }, + ], + }, + }, + }, + intArrArrArr: { + array_val: { + array_data: { + data: [ + { + array_data: { + data: [ + { long_data: { data: [1, 2] } }, + { long_data: { data: [3, 4] } }, + ], + }, + }, + { + array_data: { + data: [ + { long_data: { data: [5, 6] } }, + { long_data: { data: [7, 8] } }, + ], + }, + }, + ], + }, + }, + }, + defaultArr: { + array_val: { + string_data: { data: [undefined, undefined] }, + }, + }, + }); + }); });