From 87e9a15dce96846f539c0dc77329a660cc0f9580 Mon Sep 17 00:00:00 2001 From: jwierzbo Date: Wed, 25 Jan 2023 16:21:00 +0100 Subject: [PATCH 01/14] GT-318 Implement Index API in V2 drivers --- v2/arangodb/analyzers.go | 40 +++ v2/arangodb/collection.go | 1 + v2/arangodb/collection_documents_create.go | 4 +- v2/arangodb/collection_documents_read.go | 4 +- v2/arangodb/collection_documents_update.go | 4 +- v2/arangodb/collection_impl.go | 2 + v2/arangodb/collection_indexe_inverted.go | 173 ++++++++++++ v2/arangodb/collection_indexes.go | 266 ++++++++++++++++++ v2/arangodb/collection_indexes_impl.go | 310 +++++++++++++++++++++ v2/arangodb/shared.go | 127 +++++++++ v2/tests/helper_test.go | 4 +- 11 files changed, 927 insertions(+), 8 deletions(-) create mode 100644 v2/arangodb/analyzers.go create mode 100644 v2/arangodb/collection_indexe_inverted.go create mode 100644 v2/arangodb/collection_indexes.go create mode 100644 v2/arangodb/collection_indexes_impl.go create mode 100644 v2/arangodb/shared.go diff --git a/v2/arangodb/analyzers.go b/v2/arangodb/analyzers.go new file mode 100644 index 00000000..ea550596 --- /dev/null +++ b/v2/arangodb/analyzers.go @@ -0,0 +1,40 @@ +// +// DISCLAIMER +// +// Copyright 2020 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Jakub Wierzbowski +// + +package arangodb + +// AnalyzerFeature specifies a feature to an analyzer +type AnalyzerFeature string + +const ( + // AnalyzerFeatureFrequency how often a term is seen, required for PHRASE() + AnalyzerFeatureFrequency AnalyzerFeature = "frequency" + + // AnalyzerFeatureNorm the field normalization factor + AnalyzerFeatureNorm AnalyzerFeature = "norm" + + // AnalyzerFeaturePosition sequentially increasing term position, required for PHRASE(). If present then the frequency feature is also required + AnalyzerFeaturePosition AnalyzerFeature = "position" + + // AnalyzerFeatureOffset can be specified if 'position' feature is set + AnalyzerFeatureOffset AnalyzerFeature = "offset" +) diff --git a/v2/arangodb/collection.go b/v2/arangodb/collection.go index 4b847ae9..81eabe8f 100644 --- a/v2/arangodb/collection.go +++ b/v2/arangodb/collection.go @@ -37,4 +37,5 @@ type Collection interface { Remove(ctx context.Context) error CollectionDocuments + CollectionIndexes } diff --git a/v2/arangodb/collection_documents_create.go b/v2/arangodb/collection_documents_create.go index 96d38ae7..b1df1d36 100644 --- a/v2/arangodb/collection_documents_create.go +++ b/v2/arangodb/collection_documents_create.go @@ -38,7 +38,7 @@ type CollectionDocumentCreate interface { // A ConflictError is returned when a `_key` field contains a duplicate key, other any other field violates an index constraint. CreateDocument(ctx context.Context, document interface{}) (CollectionDocumentCreateResponse, error) - // CreateDocument creates a single document in the collection. + // CreateDocumentWithOptions creates a single document in the collection. // The document data is loaded from the given document, the document meta data is returned. // If the document data already contains a `_key` field, this will be used as key of the new document, // otherwise a unique key is created. @@ -57,7 +57,7 @@ type CollectionDocumentCreate interface { // If the create request itself fails or one of the arguments is invalid, an error is returned. CreateDocuments(ctx context.Context, documents interface{}) (CollectionDocumentCreateResponseReader, error) - // CreateDocuments creates multiple documents in the collection. + // CreateDocumentsWithOptions creates multiple documents in the collection. // The document data is loaded from the given documents slice, the documents meta data is returned. // If a documents element already contains a `_key` field, this will be used as key of the new document, // otherwise a unique key is created. diff --git a/v2/arangodb/collection_documents_read.go b/v2/arangodb/collection_documents_read.go index d9796032..3565b01c 100644 --- a/v2/arangodb/collection_documents_read.go +++ b/v2/arangodb/collection_documents_read.go @@ -34,7 +34,7 @@ type CollectionDocumentRead interface { // If no document exists with given key, a NotFoundError is returned. ReadDocument(ctx context.Context, key string, result interface{}) (DocumentMeta, error) - // ReadDocument reads a single document with given key from the collection. + // ReadDocumentWithOptions reads a single document with given key from the collection. // The document data is stored into result, the document meta data is returned. // If no document exists with given key, a NotFoundError is returned. ReadDocumentWithOptions(ctx context.Context, key string, result interface{}, opts *CollectionDocumentReadOptions) (DocumentMeta, error) @@ -45,7 +45,7 @@ type CollectionDocumentRead interface { // If no document exists with a given key, a NotFoundError is returned at its errors index. ReadDocuments(ctx context.Context, keys []string) (CollectionDocumentReadResponseReader, error) - // ReadDocuments reads multiple documents with given keys from the collection. + // ReadDocumentsWithOptions reads multiple documents with given keys from the collection. // The documents data is stored into elements of the given results slice, // the documents meta data is returned. // If no document exists with a given key, a NotFoundError is returned at its errors index. diff --git a/v2/arangodb/collection_documents_update.go b/v2/arangodb/collection_documents_update.go index 4f6f5dc6..c7293f41 100644 --- a/v2/arangodb/collection_documents_update.go +++ b/v2/arangodb/collection_documents_update.go @@ -36,7 +36,7 @@ type CollectionDocumentUpdate interface { // If no document exists with given key, a NotFoundError is returned. UpdateDocument(ctx context.Context, key string, document interface{}) (CollectionDocumentUpdateResponse, error) - // UpdateDocument updates a single document with given key in the collection. + // UpdateDocumentWithOptions updates a single document with given key in the collection. // The document meta data is returned. // If no document exists with given key, a NotFoundError is returned. UpdateDocumentWithOptions(ctx context.Context, key string, document interface{}, options *CollectionDocumentUpdateOptions) (CollectionDocumentUpdateResponse, error) @@ -47,7 +47,7 @@ type CollectionDocumentUpdate interface { // If keys is nil, each element in the updates slice must contain a `_key` field. UpdateDocuments(ctx context.Context, documents interface{}) (CollectionDocumentUpdateResponseReader, error) - // UpdateDocuments updates multiple document with given keys in the collection. + // UpdateDocumentsWithOptions updates multiple document with given keys in the collection. // The updates are loaded from the given updates slice, the documents meta data are returned. // If no document exists with a given key, a NotFoundError is returned at its errors index. // If keys is nil, each element in the updates slice must contain a `_key` field. diff --git a/v2/arangodb/collection_impl.go b/v2/arangodb/collection_impl.go index c0854eb1..712001e3 100644 --- a/v2/arangodb/collection_impl.go +++ b/v2/arangodb/collection_impl.go @@ -37,6 +37,7 @@ func newCollection(db *database, name string, modifiers ...connection.RequestMod d := &collection{db: db, name: name, modifiers: append(db.modifiers, modifiers...)} d.collectionDocuments = newCollectionDocuments(d) + d.collectionIndexes = newCollectionIndexes(d) return d } @@ -51,6 +52,7 @@ type collection struct { modifiers []connection.RequestModifier *collectionDocuments + *collectionIndexes } func (c collection) Remove(ctx context.Context) error { diff --git a/v2/arangodb/collection_indexe_inverted.go b/v2/arangodb/collection_indexe_inverted.go new file mode 100644 index 00000000..2ac14f33 --- /dev/null +++ b/v2/arangodb/collection_indexe_inverted.go @@ -0,0 +1,173 @@ +// +// DISCLAIMER +// +// Copyright 2020 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Jakub Wierzbowski +// + +package arangodb + +// InvertedIndexOptions provides specific options for creating an inverted index +type InvertedIndexOptions struct { + // Name optional user defined name used for hints in AQL queries + Name string `json:"name,omitempty"` + + // Fields contains the properties for individual fields of the element. + // The key of the map are field names. + // Required: true + Fields []InvertedIndexField `json:"fields,omitempty"` + + // SearchField This option only applies if you use the inverted index in a search-alias Views. + // You can set the option to true to get the same behavior as with arangosearch Views regarding the indexing of array values as the default. + // If enabled, both, array and primitive values (strings, numbers, etc.) are accepted. Every element of an array is indexed according to the trackListPositions option. + // If set to false, it depends on the attribute path. If it explicitly expand an array ([*]), then the elements are indexed separately. + // Otherwise, the array is indexed as a whole, but only geopoint and aql Analyzers accept array inputs. + // You cannot use an array expansion if searchField is enabled. + SearchField *bool `json:"searchField,omitempty"` + + // Cache - Enable this option to always cache the field normalization values in memory for all fields by default. + Cache *bool `json:"cache,omitempty"` + + // StoredValues The optional storedValues attribute can contain an array of paths to additional attributes to store in the index. + // These additional attributes cannot be used for index lookups or for sorting, but they can be used for projections. + // This allows an index to fully cover more queries and avoid extra document lookups. + StoredValues []StoredValue `json:"storedValues,omitempty"` + + // PrimarySort You can define a primary sort order to enable an AQL optimization. + // If a query iterates over all documents of a collection, wants to sort them by attribute values, and the (left-most) fields to sort by, + // as well as their sorting direction, match with the primarySort definition, then the SORT operation is optimized away. + PrimarySort *PrimarySort `json:"primarySort,omitempty"` + + // PrimaryKeyCache Enable this option to always cache the primary key column in memory. + // This can improve the performance of queries that return many documents. + PrimaryKeyCache *bool `json:"primaryKeyCache,omitempty"` + + // Analyzer The name of an Analyzer to use by default. This Analyzer is applied to the values of the indexed + // fields for which you don’t define Analyzers explicitly. + Analyzer string `json:"analyzer,omitempty"` + + // Features list of analyzer features. You can set this option to overwrite what features are enabled for the default analyzer + Features []AnalyzerFeature `json:"features,omitempty"` + + // IncludeAllFields If set to true, all fields of this element will be indexed. Defaults to false. + // Warning: Using includeAllFields for a lot of attributes in combination with complex Analyzers + // may significantly slow down the indexing process. + IncludeAllFields *bool `json:"includeAllFields,omitempty"` + + // TrackListPositions track the value position in arrays for array values. + TrackListPositions *bool `json:"trackListPositions,omitempty"` + + // Parallelism - The number of threads to use for indexing the fields. Default: 2 + Parallelism *int `json:"parallelism,omitempty"` + + // CleanupIntervalStep Wait at least this many commits between removing unused files in the ArangoSearch data directory + // (default: 2, to disable use: 0). + CleanupIntervalStep *int64 `json:"cleanupIntervalStep,omitempty"` + + // CommitIntervalMsec Wait at least this many milliseconds between committing View data store changes and making + // documents visible to queries (default: 1000, to disable use: 0). + CommitIntervalMsec *int64 `json:"commitIntervalMsec,omitempty"` + + // ConsolidationIntervalMsec Wait at least this many milliseconds between applying ‘consolidationPolicy’ to consolidate View data store + // and possibly release space on the filesystem (default: 1000, to disable use: 0). + ConsolidationIntervalMsec *int64 `json:"consolidationIntervalMsec,omitempty"` + + // ConsolidationPolicy The consolidation policy to apply for selecting which segments should be merged (default: {}). + ConsolidationPolicy *ConsolidationPolicy `json:"consolidationPolicy,omitempty"` + + // WriteBufferIdle Maximum number of writers (segments) cached in the pool (default: 64, use 0 to disable) + WriteBufferIdle *int64 `json:"writebufferIdle,omitempty"` + + // WriteBufferActive Maximum number of concurrent active writers (segments) that perform a transaction. + // Other writers (segments) wait till current active writers (segments) finish (default: 0, use 0 to disable) + WriteBufferActive *int64 `json:"writebufferActive,omitempty"` + + // WriteBufferSizeMax Maximum memory byte size per writer (segment) before a writer (segment) flush is triggered. + // 0 value turns off this limit for any writer (buffer) and data will be flushed periodically based on the value defined for the flush thread (ArangoDB server startup option). + // 0 value should be used carefully due to high potential memory consumption (default: 33554432, use 0 to disable) + WriteBufferSizeMax *int64 `json:"writebufferSizeMax,omitempty"` +} + +// InvertedIndexField contains configuration for indexing of the field +type InvertedIndexField struct { + // Name (Required) An attribute path. The '.' character denotes sub-attributes. + Name string `json:"name"` + + // Analyzer indicating the name of an analyzer instance + // Default: the value defined by the top-level analyzer option, or if not set, the default identity Analyzer. + Analyzer string `json:"analyzer,omitempty"` + + // Features is a list of Analyzer features to use for this field. They define what features are enabled for the analyzer + Features []AnalyzerFeature `json:"features,omitempty"` + + // IncludeAllFields This option only applies if you use the inverted index in a search-alias Views. + // If set to true, then all sub-attributes of this field are indexed, excluding any sub-attributes that are configured separately by other elements in the fields array (and their sub-attributes). The analyzer and features properties apply to the sub-attributes. + // If set to false, then sub-attributes are ignored. The default value is defined by the top-level includeAllFields option, or false if not set. + IncludeAllFields *bool `json:"includeAllFields,omitempty"` + + // SearchField This option only applies if you use the inverted index in a search-alias Views. + // You can set the option to true to get the same behavior as with arangosearch Views regarding the indexing of array values for this field. If enabled, both, array and primitive values (strings, numbers, etc.) are accepted. Every element of an array is indexed according to the trackListPositions option. + // If set to false, it depends on the attribute path. If it explicitly expand an array ([*]), then the elements are indexed separately. Otherwise, the array is indexed as a whole, but only geopoint and aql Analyzers accept array inputs. You cannot use an array expansion if searchField is enabled. + // Default: the value defined by the top-level searchField option, or false if not set. + SearchField *bool `json:"searchField,omitempty"` + + // TrackListPositions This option only applies if you use the inverted index in a search-alias Views. + // If set to true, then track the value position in arrays for array values. For example, when querying a document like { attr: [ "valueX", "valueY", "valueZ" ] }, you need to specify the array element, e.g. doc.attr[1] == "valueY". + // If set to false, all values in an array are treated as equal alternatives. You don’t specify an array element in queries, e.g. doc.attr == "valueY", and all elements are searched for a match. + // Default: the value defined by the top-level trackListPositions option, or false if not set. + TrackListPositions *bool `json:"trackListPositions,omitempty"` + + // Cache - Enable this option to always cache the field normalization values in memory for this specific field + // Default: the value defined by the top-level 'cache' option. + Cache *bool `json:"cache,omitempty"` + + // Nested Index the specified sub-objects that are stored in an array. + // Other than with the fields property, the values get indexed in a way that lets you query for co-occurring values. + // For example, you can search the sub-objects and all the conditions need to be met by a single sub-object instead of across all of them. + // Enterprise-only feature + Nested []InvertedIndexNestedField `json:"nested,omitempty"` +} + +// InvertedIndexNestedField contains sub-object configuration for indexing of the field +type InvertedIndexNestedField struct { + // Name An attribute path. The . character denotes sub-attributes. + Name string `json:"name"` + + // Analyzer indicating the name of an analyzer instance + // Default: the value defined by the top-level analyzer option, or if not set, the default identity Analyzer. + Analyzer string `json:"analyzer,omitempty"` + + // Features is a list of Analyzer features to use for this field. They define what features are enabled for the analyzer + Features []AnalyzerFeature `json:"features,omitempty"` + + // SearchField This option only applies if you use the inverted index in a search-alias Views. + // You can set the option to true to get the same behavior as with arangosearch Views regarding the indexing of array values for this field. If enabled, both, array and primitive values (strings, numbers, etc.) are accepted. Every element of an array is indexed according to the trackListPositions option. + // If set to false, it depends on the attribute path. If it explicitly expand an array ([*]), then the elements are indexed separately. Otherwise, the array is indexed as a whole, but only geopoint and aql Analyzers accept array inputs. You cannot use an array expansion if searchField is enabled. + // Default: the value defined by the top-level searchField option, or false if not set. + SearchField *bool `json:"searchField,omitempty"` + + // Cache - Enable this option to always cache the field normalization values in memory for this specific field + // Default: the value defined by the top-level 'cache' option. + Cache *bool `json:"cache,omitempty"` + + // Nested - Index the specified sub-objects that are stored in an array. + // Other than with the fields property, the values get indexed in a way that lets you query for co-occurring values. + // For example, you can search the sub-objects and all the conditions need to be met by a single sub-object instead of across all of them. + // Enterprise-only feature + Nested []InvertedIndexNestedField `json:"nested,omitempty"` +} diff --git a/v2/arangodb/collection_indexes.go b/v2/arangodb/collection_indexes.go new file mode 100644 index 00000000..6521da90 --- /dev/null +++ b/v2/arangodb/collection_indexes.go @@ -0,0 +1,266 @@ +// +// DISCLAIMER +// +// Copyright 2020 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Jakub Wierzbowski +// + +package arangodb + +import ( + "context" +) + +// CollectionIndexes provides access to the indexes in a single collection. +type CollectionIndexes interface { + // Index opens a connection to an existing index within the collection. + // If no index with given name exists, an NotFoundError is returned. + Index(ctx context.Context, name string) (IndexResponse, error) + + // IndexExists returns true if an index with given name exists within the collection. + IndexExists(ctx context.Context, name string) (bool, error) + + // Indexes returns a list of all indexes in the collection. + Indexes(ctx context.Context) ([]IndexResponse, error) + + // EnsurePersistentIndex creates a persistent index in the collection, if it does not already exist. + // Fields is a slice of attribute paths. + // The index is returned, together with a boolean indicating if the index was newly created (true) or pre-existing (false). + // NOTE: 'hash' and 'skiplist' being mere aliases for the persistent index type nowadays + EnsurePersistentIndex(ctx context.Context, fields []string, options *CreatePersistentIndexOptions) (IndexResponse, bool, error) + + // EnsureGeoIndex creates a hash index in the collection, if it does not already exist. + // Fields is a slice with one or two attribute paths. If it is a slice with one attribute path location, + // then a geo-spatial index on all documents is created using location as path to the coordinates. + // The value of the attribute must be a slice with at least two double values. The slice must contain the latitude (first value) + // and the longitude (second value). All documents, which do not have the attribute path or with value that are not suitable, are ignored. + // If it is a slice with two attribute paths latitude and longitude, then a geo-spatial index on all documents is created + // using latitude and longitude as paths the latitude and the longitude. The value of the attribute latitude and of the + // attribute longitude must a double. All documents, which do not have the attribute paths or which values are not suitable, are ignored. + // The index is returned, together with a boolean indicating if the index was newly created (true) or pre-existing (false). + EnsureGeoIndex(ctx context.Context, fields []string, options *CreateGeoIndexOptions) (IndexResponse, bool, error) + + // EnsureTTLIndex creates a TLL collection, if it does not already exist. + // The index is returned, together with a boolean indicating if the index was newly created (true) or pre-existing (false). + EnsureTTLIndex(ctx context.Context, fields []string, expireAfter int, options *CreateTTLIndexOptions) (IndexResponse, bool, error) + + // EnsureZKDIndex creates a ZKD multi-dimensional index for the collection, if it does not already exist. + // The index is returned, together with a boolean indicating if the index was newly created (true) or pre-existing (false). + EnsureZKDIndex(ctx context.Context, fields []string, options *CreateZKDIndexOptions) (IndexResponse, bool, error) + + // EnsureInvertedIndex creates an inverted index in the collection, if it does not already exist. + // The index is returned, together with a boolean indicating if the index was newly created (true) or pre-existing (false). + // Available in ArangoDB 3.10 and later. + // InvertedIndexOptions is an obligatory parameter and must contain at least `Fields` field + EnsureInvertedIndex(ctx context.Context, options *InvertedIndexOptions) (IndexResponse, bool, error) + + // DeleteIndex deletes an index from the collection. + DeleteIndex(ctx context.Context, name string) error + + // DeleteIndexById deletes an index from the collection. + DeleteIndexById(ctx context.Context, id string) error +} + +// IndexType represents an index type as string +type IndexType string + +const ( + // PrimaryIndexType is automatically created for each collections. It indexes the documents’ primary keys, + // which are stored in the _key system attribute. The primary index is unique and can be used for queries on both the _key and _id attributes. + // There is no way to explicitly create or delete primary indexes. + PrimaryIndexType = IndexType("primary") + + // EdgeIndexType is automatically created for edge collections. It contains connections between vertex documents + // and is invoked when the connecting edges of a vertex are queried. There is no way to explicitly create or delete edge indexes. + // The edge index is non-unique. + EdgeIndexType = IndexType("edge") + + // PersistentIndexType is a sorted index that can be used for finding individual documents or ranges of documents. + PersistentIndexType = IndexType("persistent") + + // GeoIndexType index can accelerate queries that filter and sort by the distance between stored coordinates and coordinates provided in a query. + GeoIndexType = IndexType("geo") + + // TTLIndexType index can be used for automatically removing expired documents from a collection. + // Documents which are expired are eventually removed by a background thread. + TTLIndexType = IndexType("ttl") + + // ZKDIndexType == multi-dimensional index. The zkd index type is an experimental index for indexing two- or higher dimensional data such as time ranges, + // for efficient intersection of multiple range queries. + ZKDIndexType = IndexType("zkd") + + // InvertedIndexType can be used to speed up a broad range of AQL queries, from simple to complex, including full-text search + InvertedIndexType = IndexType("inverted") + + /*** DEPRECATED INDEXES ***/ + + // FullTextIndex - Deprecated: since 3.10 version. Use ArangoSearch view instead. + FullTextIndex = IndexType("fulltext") + + // HashIndex are an aliases for the persistent index type and should no longer be used to create new indexes. + // The aliases will be removed in a future version. + HashIndex = IndexType("hash") + + // SkipListIndex are an aliases for the persistent index type and should no longer be used to create new indexes. + // The aliases will be removed in a future version. + SkipListIndex = IndexType("skiplist") + + /*** DEPRECATED INDEXES ***/ +) + +// IndexResponse is the response from the Index list method +type IndexResponse struct { + // Name optional user defined name used for hints in AQL queries + Name string `json:"name,omitempty"` + + // Type returns the type of the index + Type IndexType `json:"type"` + + IndexSharedOptions `json:",inline"` + + // RegularIndex is the regular index object. It is empty for the InvertedIndex type. + RegularIndex *IndexOptions `json:"indexes"` + + // InvertedIndex is the inverted index object. It is not empty only for InvertedIndex type. + InvertedIndex *InvertedIndexOptions `json:"invertedIndexes"` +} + +// IndexSharedOptions contains options that are shared between all index types +type IndexSharedOptions struct { + // ID returns the ID of the index. Effectively this is `/`. + ID string `json:"id,omitempty"` + + // Unique is supported by persistent indexes. By default, all user-defined indexes are non-unique. + // Only the attributes in fields are checked for uniqueness. + // Any attributes in from storedValues are not checked for their uniqueness. + Unique *bool `json:"unique,omitempty"` + + // Sparse You can control the sparsity for persistent indexes. + // The inverted, fulltext, and geo index types are sparse by definition. + Sparse *bool `json:"sparse,omitempty"` + + // IsNewlyCreated returns if this index was newly created or pre-existing. + IsNewlyCreated *bool `json:"isNewlyCreated,omitempty"` +} + +// IndexOptions contains the information about an regular index type +type IndexOptions struct { + // Fields returns a list of attributes of this index. + Fields []string `json:"fields,omitempty"` + + // Estimates determines if the to-be-created index should maintain selectivity estimates or not - PersistentIndex only + Estimates *bool `json:"estimates,omitempty"` + + // SelectivityEstimate determines the selectivity estimate value of the index - PersistentIndex only + SelectivityEstimate float64 `json:"selectivityEstimate,omitempty"` + + // MinLength returns min length for this index if set. + MinLength *int `json:"minLength,omitempty"` + + // Deduplicate returns deduplicate setting of this index. + Deduplicate *bool `json:"deduplicate,omitempty"` + + // ExpireAfter returns an expiry after for this index if set. + ExpireAfter *int `json:"expireAfter,omitempty"` + + // CacheEnabled if true, then the index will be cached in memory. Caching is turned off by default. + CacheEnabled *bool `json:"cacheEnabled,omitempty"` + + // StoredValues returns a list of stored values for this index - PersistentIndex only + StoredValues []string `json:"storedValues,omitempty"` + + // GeoJSON returns if geo json was set for this index or not. + GeoJSON *bool `json:"geoJson,omitempty"` + + // LegacyPolygons returns if legacy polygons was set for this index or not before 3.10 - GeoIndex only + LegacyPolygons *bool `json:"legacyPolygons,omitempty"` +} + +// CreatePersistentIndexOptions contains specific options for creating a persistent index. +// Note: "hash" and "skiplist" are only aliases for "persistent" with the RocksDB storage engine which is only storage engine since 3.7 +type CreatePersistentIndexOptions struct { + // Name optional user defined name used for hints in AQL queries + Name string `json:"name,omitempty"` + + // CacheEnabled if true, then the index will be cached in memory. Caching is turned off by default. + CacheEnabled *bool `json:"cacheEnabled,omitempty"` + + // StoreValues if true, then the additional attributes will be included. + // These additional attributes cannot be used for index lookups or sorts, but they can be used for projections. + // There must be no overlap of attribute paths between `fields` and `storedValues`. The maximum number of values is 32. + StoredValues []string `json:"storedValues,omitempty"` + + // Sparse You can control the sparsity for persistent indexes. + // The inverted, fulltext, and geo index types are sparse by definition. + Sparse *bool `json:"sparse,omitempty"` + + // Unique is supported by persistent indexes. By default, all user-defined indexes are non-unique. + // Only the attributes in fields are checked for uniqueness. + // Any attributes in from storedValues are not checked for their uniqueness. + Unique *bool `json:"unique,omitempty"` + + // Deduplicate is supported by array indexes of type persistent. It controls whether inserting duplicate index + // values from the same document into a unique array index will lead to a unique constraint error or not. + // The default value is true, so only a single instance of each non-unique index value will be inserted into + // the index per document. + // Trying to insert a value into the index that already exists in the index will always fail, + // regardless of the value of this attribute. + Deduplicate *bool `json:"deduplicate,omitempty"` + + // Estimates determines if the to-be-created index should maintain selectivity estimates or not. + // Is supported by indexes of type persistent + // This attribute controls whether index selectivity estimates are maintained for the index. + // Not maintaining index selectivity estimates can have a slightly positive impact on write performance. + // The downside of turning off index selectivity estimates will be that the query optimizer will not be able + // to determine the usefulness of different competing indexes in AQL queries when there are multiple candidate + // indexes to choose from. The estimates attribute is optional and defaults to true if not set. + // It will have no effect on indexes other than persistent (with hash and skiplist being mere aliases for the persistent index type nowadays). + Estimates *bool +} + +// CreateGeoIndexOptions contains specific options for creating a geo index. +type CreateGeoIndexOptions struct { + // Name optional user defined name used for hints in AQL queries + Name string `json:"name,omitempty"` + + // If a geo-spatial index on a location is constructed and GeoJSON is true, then the order within the array + // is longitude followed by latitude. This corresponds to the format described in http://geojson.org/geojson-spec.html#positions + GeoJSON *bool `json:"geoJson,omitempty"` + + // LegacyPolygons determines if the to-be-created index should use legacy polygons or not. + // It is relevant for those that have geoJson set to true only. + // Old geo indexes from versions from below 3.10 will always implicitly have the legacyPolygons option set to true. + // Newly generated geo indexes from 3.10 on will have the legacyPolygons option by default set to false, + // however, it can still be explicitly overwritten with true to create a legacy index but is not recommended. + LegacyPolygons *bool `json:"legacyPolygons,omitempty"` +} + +// CreateTTLIndexOptions provides specific options for creating a TTL index +type CreateTTLIndexOptions struct { + // Name optional user defined name used for hints in AQL queries + Name string `json:"name,omitempty"` +} + +// CreateZKDIndexOptions provides specific options for creating a ZKD index +type CreateZKDIndexOptions struct { + // Name optional user defined name used for hints in AQL queries + Name string `json:"name,omitempty"` + + // FieldValueTypes is required and the only allowed value is "double". Future extensions of the index will allow other types. + FieldValueTypes string `json:"fieldValueTypes,required"` +} diff --git a/v2/arangodb/collection_indexes_impl.go b/v2/arangodb/collection_indexes_impl.go new file mode 100644 index 00000000..d10746bc --- /dev/null +++ b/v2/arangodb/collection_indexes_impl.go @@ -0,0 +1,310 @@ +// +// DISCLAIMER +// +// Copyright 2020 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Jakub Wierzbowski +// + +package arangodb + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/pkg/errors" + + "github.com/arangodb/go-driver/v2/arangodb/shared" + "github.com/arangodb/go-driver/v2/connection" +) + +func newCollectionIndexes(collection *collection) *collectionIndexes { + d := &collectionIndexes{collection: collection} + return d +} + +var ( + _ CollectionIndexes = &collectionIndexes{} +) + +type collectionIndexes struct { + collection *collection +} + +func (c *collectionIndexes) Index(ctx context.Context, name string) (IndexResponse, error) { + url := c.collection.url("index", name) + + var response struct { + shared.ResponseStruct `json:",inline"` + IndexResponse `json:",inline"` + } + + resp, err := connection.CallGet(ctx, c.collection.connection(), url, &response, c.collection.withModifiers()...) + if err != nil { + return IndexResponse{}, errors.WithStack(err) + } + + switch code := resp.Code(); code { + case http.StatusOK: + return response.IndexResponse, nil + default: + return IndexResponse{}, response.AsArangoErrorWithCode(code) + } +} + +func (c *collectionIndexes) IndexExists(ctx context.Context, name string) (bool, error) { + url := c.collection.url("index", name) + + resp, err := connection.CallGet(ctx, c.collection.connection(), url, nil, c.collection.withModifiers()...) + if err != nil { + if connection.IsNotFoundError(err) { + return false, nil + } + return false, err + } + + switch code := resp.Code(); code { + case http.StatusOK: + return true, nil + case http.StatusNotFound: + return false, nil + default: + return false, shared.NewResponseStruct().AsArangoErrorWithCode(code) + } +} + +func (c *collectionIndexes) Indexes(ctx context.Context) ([]IndexResponse, error) { + url := c.collection.db.url("_api", "index") + + var response struct { + shared.ResponseStruct `json:",inline"` + Indexes []IndexResponse `json:"indexes,omitempty"` + } + + resp, err := connection.CallGet(ctx, c.collection.connection(), url, &response, + c.collection.withModifiers(connection.WithQuery("collection", c.collection.name))...) + if err != nil { + return nil, errors.WithStack(err) + } + + switch code := resp.Code(); code { + case http.StatusOK: + return response.Indexes, nil + default: + return nil, response.AsArangoErrorWithCode(code) + } +} + +func (c *collectionIndexes) EnsurePersistentIndex(ctx context.Context, fields []string, options *CreatePersistentIndexOptions) (IndexResponse, bool, error) { + reqData := struct { + Type IndexType `json:"type"` + Fields []string `json:"fields"` + *CreatePersistentIndexOptions + }{ + Type: PersistentIndexType, + Fields: fields, + CreatePersistentIndexOptions: options, + } + + result := responseIndex{} + exist, err := c.ensureIndex(ctx, &reqData, &result) + return newIndexResponse(&result), exist, err +} + +func (c *collectionIndexes) EnsureGeoIndex(ctx context.Context, fields []string, options *CreateGeoIndexOptions) (IndexResponse, bool, error) { + reqData := struct { + Type IndexType `json:"type"` + Fields []string `json:"fields"` + *CreateGeoIndexOptions + }{ + Type: GeoIndexType, + Fields: fields, + CreateGeoIndexOptions: options, + } + + result := responseIndex{} + exist, err := c.ensureIndex(ctx, &reqData, &result) + return newIndexResponse(&result), exist, err +} + +func (c *collectionIndexes) EnsureTTLIndex(ctx context.Context, fields []string, expireAfter int, options *CreateTTLIndexOptions) (IndexResponse, bool, error) { + reqData := struct { + Type IndexType `json:"type"` + Fields []string `json:"fields"` + ExpireAfter int `json:"expireAfter"` + *CreateTTLIndexOptions + }{ + Type: TTLIndexType, + Fields: fields, + ExpireAfter: expireAfter, + CreateTTLIndexOptions: options, + } + + result := responseIndex{} + exist, err := c.ensureIndex(ctx, &reqData, &result) + return newIndexResponse(&result), exist, err +} + +func (c *collectionIndexes) EnsureZKDIndex(ctx context.Context, fields []string, options *CreateZKDIndexOptions) (IndexResponse, bool, error) { + reqData := struct { + Type IndexType `json:"type"` + Fields []string `json:"fields"` + *CreateZKDIndexOptions + }{ + Type: ZKDIndexType, + Fields: fields, + CreateZKDIndexOptions: options, + } + + result := responseIndex{} + exist, err := c.ensureIndex(ctx, &reqData, &result) + return newIndexResponse(&result), exist, err +} + +func (c *collectionIndexes) EnsureInvertedIndex(ctx context.Context, options *InvertedIndexOptions) (IndexResponse, bool, error) { + if options == nil || options.Fields == nil || len(options.Fields) == 0 { + return IndexResponse{}, false, errors.New("InvertedIndexOptions with non-empty Fields are required") + } + + reqData := struct { + Type IndexType `json:"type"` + *InvertedIndexOptions + }{ + Type: InvertedIndexType, + InvertedIndexOptions: options, + } + + result := responseInvertedIndex{} + exist, err := c.ensureIndex(ctx, &reqData, &result) + return newInvertedIndexResponse(&result), exist, err +} + +func (c *collectionIndexes) ensureIndex(ctx context.Context, reqData interface{}, result interface{}) (bool, error) { + url := c.collection.db.url("_api", "index") + + var response struct { + shared.ResponseStruct `json:",inline"` + } + data := newUnmarshalInto(&result) + + resp, err := connection.CallPost(ctx, c.collection.connection(), url, newMultiUnmarshaller(&response, data), &reqData, + c.collection.withModifiers(connection.WithQuery("collection", c.collection.name))...) + if err != nil { + return false, errors.WithStack(err) + } + + switch code := resp.Code(); code { + case http.StatusOK: + return false, nil + case http.StatusCreated: + return true, nil + default: + return false, response.AsArangoErrorWithCode(code) + } +} + +func (c *collectionIndexes) DeleteIndex(ctx context.Context, name string) error { + idx, err := c.Index(ctx, name) + if err != nil { + return errors.WithStack(err) + } + + return c.DeleteIndexById(ctx, idx.ID) +} + +func (c *collectionIndexes) DeleteIndexById(ctx context.Context, id string) error { + url := c.collection.db.url("_api", "index", id) + + response := shared.ResponseStruct{} + resp, err := connection.CallDelete(ctx, c.collection.connection(), url, &response, c.collection.withModifiers()...) + if err != nil { + return errors.WithStack(err) + } + + switch code := resp.Code(); code { + case http.StatusOK: + return nil + default: + return response.AsArangoErrorWithCode(code) + } +} + +type responseIndex struct { + Name string `json:"name,omitempty"` + Type IndexType `json:"type"` + IndexSharedOptions `json:",inline"` + IndexOptions `json:",inline"` +} + +type responseInvertedIndex struct { + Name string `json:"name,omitempty"` + Type IndexType `json:"type"` + IndexSharedOptions `json:",inline"` + InvertedIndexOptions `json:",inline"` +} + +func newIndexResponse(res *responseIndex) IndexResponse { + return IndexResponse{ + Name: res.Name, + Type: res.Type, + IndexSharedOptions: res.IndexSharedOptions, + RegularIndex: &res.IndexOptions, + } +} + +func newInvertedIndexResponse(res *responseInvertedIndex) IndexResponse { + return IndexResponse{ + Name: res.Name, + Type: res.Type, + IndexSharedOptions: res.IndexSharedOptions, + InvertedIndex: &res.InvertedIndexOptions, + } +} + +func (i *IndexResponse) UnmarshalJSON(data []byte) error { + var respSimple struct { + Type IndexType `json:"type"` + Name string `json:"name"` + } + if err := json.Unmarshal(data, &respSimple); err != nil { + return err + } + + i.Name = respSimple.Name + i.Type = respSimple.Type + + if respSimple.Type == InvertedIndexType { + result := responseInvertedIndex{} + if err := json.Unmarshal(data, &result); err != nil { + return err + } + + i.IndexSharedOptions = result.IndexSharedOptions + i.InvertedIndex = &result.InvertedIndexOptions + } else { + result := responseIndex{} + if err := json.Unmarshal(data, &result); err != nil { + return err + } + + i.IndexSharedOptions = result.IndexSharedOptions + i.RegularIndex = &result.IndexOptions + } + + return nil +} diff --git a/v2/arangodb/shared.go b/v2/arangodb/shared.go new file mode 100644 index 00000000..aef5727c --- /dev/null +++ b/v2/arangodb/shared.go @@ -0,0 +1,127 @@ +// +// DISCLAIMER +// +// Copyright 2020 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Jakub Wierzbowski +// + +package arangodb + +// PrimarySortCompression Defines how to compress the primary sort data (introduced in v3.7.1) +type PrimarySortCompression string + +const ( + // PrimarySortCompressionLz4 (default): use LZ4 fast compression. + PrimarySortCompressionLz4 PrimarySortCompression = "lz4" + + // PrimarySortCompressionNone disable compression to trade space for speed. + PrimarySortCompressionNone PrimarySortCompression = "none" +) + +// SortDirection describes the sorting direction +type SortDirection string + +const ( + // SortDirectionAsc sort ascending + SortDirectionAsc SortDirection = "asc" + + // SortDirectionDesc sort descending + SortDirectionDesc SortDirection = "desc" +) + +// PrimarySort defines compression and list of fields to be sorted +type PrimarySort struct { + // Fields (Required) - An array of the fields to sort the index by and the direction to sort each field in. + Fields []PrimarySortEntry `json:"fields,omitempty"` + + // Compression Defines how to compress the primary sort data + Compression PrimarySortCompression `json:"compression,omitempty"` + + // Cache - Enable this option to always cache the primary sort columns in memory. + // This can improve the performance of queries that utilize the primary sort order. + Cache *bool `json:"cache,omitempty"` +} + +// PrimarySortEntry field to sort the index by and the direction +type PrimarySortEntry struct { + // Field An attribute path. The . character denotes sub-attributes. + Field string `json:"field,omitempty"` + + // Direction The sorting direction + Direction *SortDirection `json:"direction,omitempty"` +} + +// StoredValue defines the value stored in the index +type StoredValue struct { + // Fields A list of attribute paths. The . character denotes sub-attributes. + Fields []string `json:"fields,omitempty"` + + // Compression Defines how to compress the attribute values. + Compression PrimarySortCompression `json:"compression,omitempty"` + + // Cache attribute allows you to always cache stored values in memory + // Introduced in v3.9.5, Enterprise Edition only + Cache *bool `json:"cache,omitempty"` +} + +// ConsolidationPolicyType strings for consolidation types +type ConsolidationPolicyType string + +const ( + // ConsolidationPolicyTypeTier consolidate based on segment byte size and live document count as dictated by the customization attributes. + ConsolidationPolicyTypeTier ConsolidationPolicyType = "tier" + + // ConsolidationPolicyTypeBytesAccum consolidate if and only if ({threshold} range [0.0, 1.0]) + // {threshold} > (segment_bytes + sum_of_merge_candidate_segment_bytes) / all_segment_bytes, + // i.e. the sum of all candidate segment's byte size is less than the total segment byte size multiplied by the {threshold}. + ConsolidationPolicyTypeBytesAccum ConsolidationPolicyType = "bytes_accum" +) + +// ConsolidationPolicy holds threshold values specifying when to consolidate view data. +// Semantics of the values depend on where they are used. +type ConsolidationPolicy struct { + // Type returns the type of the ConsolidationPolicy. This interface can then be casted to the corresponding ConsolidationPolicy struct. + Type ConsolidationPolicyType `json:"type,omitempty"` + + ConsolidationPolicyBytesAccum + ConsolidationPolicyTier +} + +// ConsolidationPolicyBytesAccum contains fields used for ConsolidationPolicyTypeBytesAccum +type ConsolidationPolicyBytesAccum struct { + // Threshold, see ConsolidationTypeBytesAccum + Threshold *float64 `json:"threshold,omitempty"` +} + +// ConsolidationPolicyTier contains fields used for ConsolidationPolicyTypeTier +type ConsolidationPolicyTier struct { + // MinScore Filter out consolidation candidates with a score less than this. Default: 0 + MinScore *int64 `json:"minScore,omitempty"` + + // SegmentsMin The minimum number of segments that are evaluated as candidates for consolidation. Default: 1 + SegmentsMin *int64 `json:"segmentsMin,omitempty"` + + // SegmentsMax The maximum number of segments that are evaluated as candidates for consolidation. Default: 10 + SegmentsMax *int64 `json:"segmentsMax,omitempty"` + + // SegmentsBytesMax The maximum allowed size of all consolidated segments in bytes. Default: 5368709120 + SegmentsBytesMax *int64 `json:"segmentsBytesMax,omitempty"` + + // SegmentsBytesFloor Defines the value (in bytes) to treat all smaller segments as equal for consolidation selection. Default: 2097152 + SegmentsBytesFloor *int64 `json:"segmentsBytesFloor,omitempty"` +} diff --git a/v2/tests/helper_test.go b/v2/tests/helper_test.go index 6dae8311..147304e3 100644 --- a/v2/tests/helper_test.go +++ b/v2/tests/helper_test.go @@ -37,7 +37,7 @@ import ( ) func WithDatabase(t testing.TB, client arangodb.Client, opts *arangodb.CreateDatabaseOptions, f func(db arangodb.Database)) { - name := fmt.Sprintf("test-%s", uuid.New().String()) + name := fmt.Sprintf("test-DB-%s", uuid.New().String()) info(t)("Creating DB %s", name) @@ -59,7 +59,7 @@ func WithDatabase(t testing.TB, client arangodb.Client, opts *arangodb.CreateDat } func WithCollection(t testing.TB, db arangodb.Database, opts *arangodb.CreateCollectionOptions, f func(col arangodb.Collection)) { - name := fmt.Sprintf("test-%s", uuid.New().String()) + name := fmt.Sprintf("test-COL-%s", uuid.New().String()) info(t)("Creating COL %s", name) From 70ac7bfe262d5919d81957368b2ef3bb41684fb7 Mon Sep 17 00:00:00 2001 From: jwierzbo Date: Thu, 26 Jan 2023 09:49:36 +0100 Subject: [PATCH 02/14] adjust year --- v2/arangodb/analyzers.go | 2 +- v2/arangodb/collection_indexe_inverted.go | 2 +- v2/arangodb/collection_indexes.go | 2 +- v2/arangodb/collection_indexes_impl.go | 2 +- v2/arangodb/shared.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/v2/arangodb/analyzers.go b/v2/arangodb/analyzers.go index ea550596..462f8cb5 100644 --- a/v2/arangodb/analyzers.go +++ b/v2/arangodb/analyzers.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2020 ArangoDB GmbH, Cologne, Germany +// Copyright 2023 ArangoDB GmbH, Cologne, Germany // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/v2/arangodb/collection_indexe_inverted.go b/v2/arangodb/collection_indexe_inverted.go index 2ac14f33..93d6d140 100644 --- a/v2/arangodb/collection_indexe_inverted.go +++ b/v2/arangodb/collection_indexe_inverted.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2020 ArangoDB GmbH, Cologne, Germany +// Copyright 2023 ArangoDB GmbH, Cologne, Germany // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/v2/arangodb/collection_indexes.go b/v2/arangodb/collection_indexes.go index 6521da90..11436d14 100644 --- a/v2/arangodb/collection_indexes.go +++ b/v2/arangodb/collection_indexes.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2020 ArangoDB GmbH, Cologne, Germany +// Copyright 2023 ArangoDB GmbH, Cologne, Germany // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/v2/arangodb/collection_indexes_impl.go b/v2/arangodb/collection_indexes_impl.go index d10746bc..b24c98a5 100644 --- a/v2/arangodb/collection_indexes_impl.go +++ b/v2/arangodb/collection_indexes_impl.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2020 ArangoDB GmbH, Cologne, Germany +// Copyright 2023 ArangoDB GmbH, Cologne, Germany // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/v2/arangodb/shared.go b/v2/arangodb/shared.go index aef5727c..f4858c2b 100644 --- a/v2/arangodb/shared.go +++ b/v2/arangodb/shared.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2020 ArangoDB GmbH, Cologne, Germany +// Copyright 2023 ArangoDB GmbH, Cologne, Germany // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. From d6ea139fc413758b7e69702d654e4ab09cef65d0 Mon Sep 17 00:00:00 2001 From: jwierzbo Date: Fri, 27 Jan 2023 15:40:54 +0100 Subject: [PATCH 03/14] add UT --- v2/arangodb/collection_documents_impl.go | 2 + v2/arangodb/collection_indexes.go | 6 +- v2/go.mod | 1 + v2/go.sum | 2 + v2/tests/database_collection_indexes_test.go | 288 +++++++++++++++++++ 5 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 v2/tests/database_collection_indexes_test.go diff --git a/v2/arangodb/collection_documents_impl.go b/v2/arangodb/collection_documents_impl.go index 4c133ddb..401a9719 100644 --- a/v2/arangodb/collection_documents_impl.go +++ b/v2/arangodb/collection_documents_impl.go @@ -66,6 +66,8 @@ func (c collectionDocuments) DocumentExists(ctx context.Context, key string) (bo switch code := resp.Code(); code { case http.StatusOK: return true, nil + case http.StatusNotFound: + return false, nil default: return false, shared.NewResponseStruct().AsArangoErrorWithCode(code) } diff --git a/v2/arangodb/collection_indexes.go b/v2/arangodb/collection_indexes.go index 11436d14..e457e603 100644 --- a/v2/arangodb/collection_indexes.go +++ b/v2/arangodb/collection_indexes.go @@ -56,7 +56,9 @@ type CollectionIndexes interface { EnsureGeoIndex(ctx context.Context, fields []string, options *CreateGeoIndexOptions) (IndexResponse, bool, error) // EnsureTTLIndex creates a TLL collection, if it does not already exist. - // The index is returned, together with a boolean indicating if the index was newly created (true) or pre-existing (false). + // expireAfter is the time interval (in seconds) from the point in time stored in the fields attribute after which the documents count as expired. + // Can be set to 0 to let documents expire as soon as the server time passes the point in time stored in the document attribute, or to a higher number to delay the expiration. + // fields The index is returned, together with a boolean indicating if the index was newly created (true) or pre-existing (false). EnsureTTLIndex(ctx context.Context, fields []string, expireAfter int, options *CreateTTLIndexOptions) (IndexResponse, bool, error) // EnsureZKDIndex creates a ZKD multi-dimensional index for the collection, if it does not already exist. @@ -230,7 +232,7 @@ type CreatePersistentIndexOptions struct { // to determine the usefulness of different competing indexes in AQL queries when there are multiple candidate // indexes to choose from. The estimates attribute is optional and defaults to true if not set. // It will have no effect on indexes other than persistent (with hash and skiplist being mere aliases for the persistent index type nowadays). - Estimates *bool + Estimates *bool `json:"estimates,omitempty"` } // CreateGeoIndexOptions contains specific options for creating a geo index. diff --git a/v2/go.mod b/v2/go.mod index 0a152b1e..468bade9 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -9,6 +9,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.19.0 github.com/stretchr/testify v1.5.1 + golang.org/x/exp v0.0.0-20230125214544-b3c2aaf6208d golang.org/x/net v0.0.0-20190620200207-3b0461eec859 golang.org/x/text v0.3.0 ) diff --git a/v2/go.sum b/v2/go.sum index baf93940..908df6d6 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -25,6 +25,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20230125214544-b3c2aaf6208d h1:9Bio0JlZpJ1P4NXsK5i8Rf2MclrRzMGzJWOIkhZ5Um8= +golang.org/x/exp v0.0.0-20230125214544-b3c2aaf6208d/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/v2/tests/database_collection_indexes_test.go b/v2/tests/database_collection_indexes_test.go new file mode 100644 index 00000000..634b9394 --- /dev/null +++ b/v2/tests/database_collection_indexes_test.go @@ -0,0 +1,288 @@ +// +// DISCLAIMER +// +// Copyright 2023 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Jakub Wierzbowski +// + +package tests + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/exp/slices" + + "github.com/arangodb/go-driver/v2/arangodb" + "github.com/arangodb/go-driver/v2/arangodb/shared" +) + +func Test_DefaultIndexes(t *testing.T) { + Wrap(t, func(t *testing.T, client arangodb.Client) { + WithDatabase(t, client, nil, func(db arangodb.Database) { + WithCollection(t, db, nil, func(col arangodb.Collection) { + withContext(30*time.Second, func(ctx context.Context) error { + + indexes, err := col.Indexes(ctx) + require.NoError(t, err) + require.NotNil(t, indexes) + require.Equal(t, 1, len(indexes)) + assert.Equal(t, arangodb.PrimaryIndexType, indexes[0].Type) + + return nil + }) + }) + }) + }) +} + +func Test_DefaultEdgeIndexes(t *testing.T) { + Wrap(t, func(t *testing.T, client arangodb.Client) { + WithDatabase(t, client, nil, func(db arangodb.Database) { + WithCollection(t, db, &arangodb.CreateCollectionOptions{Type: arangodb.CollectionTypeEdge}, func(col arangodb.Collection) { + withContext(30*time.Second, func(ctx context.Context) error { + + indexes, err := col.Indexes(ctx) + require.NoError(t, err) + require.NotNil(t, indexes) + require.Equal(t, 2, len(indexes)) + assert.Equal(t, arangodb.PrimaryIndexType, indexes[0].Type) + + assert.True(t, slices.ContainsFunc(indexes, func(i arangodb.IndexResponse) bool { + return i.Type == arangodb.PrimaryIndexType + })) + + assert.True(t, slices.ContainsFunc(indexes, func(i arangodb.IndexResponse) bool { + return i.Type == arangodb.EdgeIndexType + })) + + return nil + }) + }) + }) + }) +} + +func Test_EnsurePersistentIndexBasicOpts(t *testing.T) { + Wrap(t, func(t *testing.T, client arangodb.Client) { + WithDatabase(t, client, nil, func(db arangodb.Database) { + WithCollection(t, db, nil, func(col arangodb.Collection) { + withContext(30*time.Second, func(ctx context.Context) error { + + var testOptions = []struct { + ShouldBeCreated bool + ExpectedNoIdx int + Fields []string + Opts *arangodb.CreatePersistentIndexOptions + }{ + // default options + {true, 2, []string{"age", "name"}, nil}, + // same as default + {false, 2, []string{"age", "name"}, + &arangodb.CreatePersistentIndexOptions{Unique: newBool(false), Sparse: newBool(false)}}, + + // unique + {true, 3, []string{"age", "name"}, + &arangodb.CreatePersistentIndexOptions{Unique: newBool(true), Sparse: newBool(false)}}, + {false, 3, []string{"age", "name"}, + &arangodb.CreatePersistentIndexOptions{Unique: newBool(true), Sparse: newBool(false)}}, + + {true, 4, []string{"age", "name"}, + &arangodb.CreatePersistentIndexOptions{Unique: newBool(true), Sparse: newBool(true)}}, + {false, 4, []string{"age", "name"}, + &arangodb.CreatePersistentIndexOptions{Unique: newBool(true), Sparse: newBool(true)}}, + + {true, 5, []string{"age", "name"}, + &arangodb.CreatePersistentIndexOptions{Unique: newBool(false), Sparse: newBool(true)}}, + {false, 5, []string{"age", "name"}, + &arangodb.CreatePersistentIndexOptions{Unique: newBool(false), Sparse: newBool(true)}}, + } + + for _, testOpt := range testOptions { + idx, created, err := col.EnsurePersistentIndex(ctx, testOpt.Fields, testOpt.Opts) + require.NoError(t, err) + require.Equal(t, created, testOpt.ShouldBeCreated) + require.Equal(t, arangodb.PersistentIndexType, idx.Type) + if testOpt.Opts != nil { + require.Equal(t, testOpt.Opts.Unique, idx.Unique) + require.Equal(t, testOpt.Opts.Sparse, idx.Sparse) + } else { + require.False(t, *idx.Unique) + require.False(t, *idx.Sparse) + } + + indexes, err := col.Indexes(ctx) + require.NoError(t, err) + require.NotNil(t, indexes) + require.Equal(t, testOpt.ExpectedNoIdx, len(indexes)) + assert.True(t, slices.ContainsFunc(indexes, func(i arangodb.IndexResponse) bool { + return i.ID == idx.ID + })) + } + + return nil + }) + }) + }) + }) +} + +func Test_EnsurePersistentIndexDeduplicate(t *testing.T) { + Wrap(t, func(t *testing.T, client arangodb.Client) { + WithDatabase(t, client, nil, func(db arangodb.Database) { + WithCollection(t, db, nil, func(col arangodb.Collection) { + withContext(30*time.Second, func(ctx context.Context) error { + doc := struct { + Tags []string `json:"tags"` + }{ + Tags: []string{"a", "a", "b"}, + } + + t.Run("Create index with Deduplicate OFF", func(t *testing.T) { + idx, created, err := col.EnsurePersistentIndex(ctx, []string{"tags[*]"}, &arangodb.CreatePersistentIndexOptions{ + Deduplicate: newBool(false), + Unique: newBool(true), + Sparse: newBool(false), + }) + require.NoError(t, err) + require.True(t, created) + require.False(t, *idx.RegularIndex.Deduplicate) + require.Equal(t, arangodb.PersistentIndexType, idx.Type) + + _, err = col.CreateDocument(ctx, doc) + require.Error(t, err) + require.True(t, shared.IsConflict(err)) + + err = col.DeleteIndexById(ctx, idx.ID) + require.NoError(t, err) + }) + + t.Run("Create index with Deduplicate ON", func(t *testing.T) { + idx, created, err := col.EnsurePersistentIndex(ctx, []string{"tags[*]"}, &arangodb.CreatePersistentIndexOptions{ + Deduplicate: newBool(true), + Unique: newBool(true), + Sparse: newBool(false), + }) + require.NoError(t, err) + require.True(t, created) + require.True(t, *idx.RegularIndex.Deduplicate) + require.Equal(t, arangodb.PersistentIndexType, idx.Type) + + _, err = col.CreateDocument(ctx, doc) + require.NoError(t, err) + + err = col.DeleteIndex(ctx, idx.Name) + require.NoError(t, err) + }) + + return nil + }) + }) + }) + }) +} + +func Test_TTLIndex(t *testing.T) { + Wrap(t, func(t *testing.T, client arangodb.Client) { + WithDatabase(t, client, nil, func(db arangodb.Database) { + WithCollection(t, db, nil, func(col arangodb.Collection) { + withContext(120*time.Second, func(ctx context.Context) error { + + t.Run("Removing documents at a fixed period after creation", func(t *testing.T) { + idx, created, err := col.EnsureTTLIndex(ctx, []string{"createdAt"}, 5, nil) + require.NoError(t, err) + require.True(t, created) + require.Equal(t, *idx.RegularIndex.ExpireAfter, 5) + require.Equal(t, arangodb.TTLIndexType, idx.Type) + + doc := struct { + CreatedAt int64 `json:"createdAt,omitempty"` + }{ + CreatedAt: time.Now().Unix(), + } + + meta, err := col.CreateDocument(ctx, doc) + require.NoError(t, err) + + exist, err := col.DocumentExists(ctx, meta.Key) + require.NoError(t, err) + require.True(t, exist) + + // cleanup is made every 30 seconds by default, so we need to wait for 35 seconds in worst case + withContext(35*time.Second, func(ctx context.Context) error { + for { + exist, err := col.DocumentExists(ctx, meta.Key) + require.NoError(t, err) + if !exist { + break + } + time.Sleep(1 * time.Second) + } + return nil + }) + + err = col.DeleteIndexById(ctx, idx.ID) + require.NoError(t, err) + }) + + t.Run("Removing documents at certain points in time", func(t *testing.T) { + idx, created, err := col.EnsureTTLIndex(ctx, []string{"expireDate"}, 0, nil) + require.NoError(t, err) + require.True(t, created) + require.Equal(t, *idx.RegularIndex.ExpireAfter, 0) + require.Equal(t, arangodb.TTLIndexType, idx.Type) + + doc := struct { + ExpireDate int64 `json:"expireDate,omitempty"` + }{ + ExpireDate: time.Now().Add(5 * time.Second).Unix(), + } + + meta, err := col.CreateDocument(ctx, doc) + require.NoError(t, err) + + exist, err := col.DocumentExists(ctx, meta.Key) + require.NoError(t, err) + require.True(t, exist) + + // cleanup is made every 30 seconds by default, so we need to wait for 35 seconds in worst case + withContext(35*time.Second, func(ctx context.Context) error { + for { + exist, err := col.DocumentExists(ctx, meta.Key) + require.NoError(t, err) + if !exist { + break + } + time.Sleep(1 * time.Second) + } + return nil + }) + + err = col.DeleteIndexById(ctx, idx.ID) + require.NoError(t, err) + }) + + return nil + }) + }) + }) + }) +} From e692a1975688d600bb04fcbf4383b93612446cd0 Mon Sep 17 00:00:00 2001 From: jwierzbo Date: Mon, 30 Jan 2023 12:36:02 +0100 Subject: [PATCH 04/14] named indexes tests --- v2/arangodb/collection_indexes.go | 6 +- v2/tests/database_collection_indexes_test.go | 84 ++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/v2/arangodb/collection_indexes.go b/v2/arangodb/collection_indexes.go index e457e603..a374cd74 100644 --- a/v2/arangodb/collection_indexes.go +++ b/v2/arangodb/collection_indexes.go @@ -258,11 +258,15 @@ type CreateTTLIndexOptions struct { Name string `json:"name,omitempty"` } +type ZKDFieldType string + +const ZKDDoubleFieldType ZKDFieldType = "double" + // CreateZKDIndexOptions provides specific options for creating a ZKD index type CreateZKDIndexOptions struct { // Name optional user defined name used for hints in AQL queries Name string `json:"name,omitempty"` // FieldValueTypes is required and the only allowed value is "double". Future extensions of the index will allow other types. - FieldValueTypes string `json:"fieldValueTypes,required"` + FieldValueTypes ZKDFieldType `json:"fieldValueTypes,required"` } diff --git a/v2/tests/database_collection_indexes_test.go b/v2/tests/database_collection_indexes_test.go index 634b9394..46673173 100644 --- a/v2/tests/database_collection_indexes_test.go +++ b/v2/tests/database_collection_indexes_test.go @@ -128,6 +128,7 @@ func Test_EnsurePersistentIndexBasicOpts(t *testing.T) { require.False(t, *idx.Unique) require.False(t, *idx.Sparse) } + assert.ElementsMatch(t, idx.RegularIndex.Fields, testOpt.Fields) indexes, err := col.Indexes(ctx) require.NoError(t, err) @@ -286,3 +287,86 @@ func Test_TTLIndex(t *testing.T) { }) }) } + +func Test_NamedIndexes(t *testing.T) { + Wrap(t, func(t *testing.T, client arangodb.Client) { + WithDatabase(t, client, nil, func(db arangodb.Database) { + WithCollection(t, db, nil, func(col arangodb.Collection) { + withContext(30*time.Second, func(ctx context.Context) error { + + var namedIndexTestCases = []struct { + Name string + CreateCallback func(col arangodb.Collection, name string) (arangodb.IndexResponse, error) + }{ + { + Name: "Persistent", + CreateCallback: func(col arangodb.Collection, name string) (arangodb.IndexResponse, error) { + idx, _, err := col.EnsurePersistentIndex(ctx, []string{"pername"}, &arangodb.CreatePersistentIndexOptions{ + Name: name, + }) + return idx, err + }, + }, + { + Name: "Geo", + CreateCallback: func(col arangodb.Collection, name string) (arangodb.IndexResponse, error) { + idx, _, err := col.EnsureGeoIndex(ctx, []string{"geo"}, &arangodb.CreateGeoIndexOptions{ + Name: name, + }) + return idx, err + }, + }, + { + Name: "TTL", + CreateCallback: func(col arangodb.Collection, name string) (arangodb.IndexResponse, error) { + idx, _, err := col.EnsureTTLIndex(ctx, []string{"createdAt"}, 3600, &arangodb.CreateTTLIndexOptions{ + Name: name, + }) + return idx, err + }, + }, + { + Name: "ZKD", + CreateCallback: func(col arangodb.Collection, name string) (arangodb.IndexResponse, error) { + idx, _, err := col.EnsureZKDIndex(ctx, []string{"zkd"}, &arangodb.CreateZKDIndexOptions{ + Name: name, + FieldValueTypes: arangodb.ZKDDoubleFieldType, + }) + return idx, err + }, + }, + { + Name: "Inverted", + CreateCallback: func(col arangodb.Collection, name string) (arangodb.IndexResponse, error) { + idx, _, err := col.EnsureInvertedIndex(ctx, &arangodb.InvertedIndexOptions{ + Name: name, + Fields: []arangodb.InvertedIndexField{ + { + Name: name, + }, + }, + }) + return idx, err + }, + }, + } + + for _, testCase := range namedIndexTestCases { + idx, err := testCase.CreateCallback(col, testCase.Name) + require.NoError(t, err) + require.Equal(t, testCase.Name, idx.Name) + + indexes, err := col.Indexes(ctx) + require.NoError(t, err) + require.NotNil(t, indexes) + assert.True(t, slices.ContainsFunc(indexes, func(i arangodb.IndexResponse) bool { + return i.ID == idx.ID && i.Name == testCase.Name + })) + } + + return nil + }) + }) + }) + }) +} From e9650b43dbd1a71fa49d882eda29788e81e02c78 Mon Sep 17 00:00:00 2001 From: jwierzbo Date: Mon, 30 Jan 2023 14:01:13 +0100 Subject: [PATCH 05/14] geoindex tests --- v2/tests/database_collection_indexes_test.go | 72 ++++++++++++++++++++ v2/tests/util_test.go | 14 ++++ 2 files changed, 86 insertions(+) diff --git a/v2/tests/database_collection_indexes_test.go b/v2/tests/database_collection_indexes_test.go index 46673173..6a7a7e3b 100644 --- a/v2/tests/database_collection_indexes_test.go +++ b/v2/tests/database_collection_indexes_test.go @@ -288,6 +288,78 @@ func Test_TTLIndex(t *testing.T) { }) } +func Test_EnsureGeoIndexIndex(t *testing.T) { + Wrap(t, func(t *testing.T, client arangodb.Client) { + WithDatabase(t, client, nil, func(db arangodb.Database) { + WithCollection(t, db, nil, func(col arangodb.Collection) { + withContext(30*time.Second, func(ctx context.Context) error { + + t.Run("Test GeoJSON opts", func(t *testing.T) { + var testOptions = []arangodb.CreateGeoIndexOptions{ + {GeoJSON: newBool(true)}, + {GeoJSON: newBool(false)}, + } + for _, testOpt := range testOptions { + idx, created, err := col.EnsureGeoIndex(ctx, []string{"geo"}, &testOpt) + require.NoError(t, err) + require.True(t, created) + require.Equal(t, arangodb.GeoIndexType, idx.Type) + require.Equal(t, testOpt.GeoJSON, idx.RegularIndex.GeoJSON) + } + }) + + t.Run("Test LegacyPolygons opts", func(t *testing.T) { + skipBelowVersion(client, ctx, "3.10", t) + var testOptions = []struct { + ExpectedLegacyPolygons bool + ExpectedGeoJSON bool + Fields []string + Opts *arangodb.CreateGeoIndexOptions + }{ + { + true, + false, + []string{"geoOld1"}, + &arangodb.CreateGeoIndexOptions{LegacyPolygons: newBool(true)}, + }, + { + false, + false, + []string{"geoOld2"}, + &arangodb.CreateGeoIndexOptions{LegacyPolygons: newBool(false)}, + }, + { + false, + true, + []string{"geoOld3"}, + &arangodb.CreateGeoIndexOptions{GeoJSON: newBool(true), LegacyPolygons: newBool(false)}, + }, + { + false, + false, + []string{"geoOld4"}, + &arangodb.CreateGeoIndexOptions{GeoJSON: newBool(false), LegacyPolygons: newBool(false)}, + }, + } + + for _, testOpt := range testOptions { + idx, created, err := col.EnsureGeoIndex(ctx, testOpt.Fields, testOpt.Opts) + require.NoError(t, err) + require.True(t, created) + require.Equal(t, arangodb.GeoIndexType, idx.Type) + assert.Equal(t, testOpt.ExpectedGeoJSON, *idx.RegularIndex.GeoJSON) + assert.Equal(t, testOpt.ExpectedLegacyPolygons, *idx.RegularIndex.LegacyPolygons) + assert.ElementsMatch(t, idx.RegularIndex.Fields, testOpt.Fields) + } + }) + + return nil + }) + }) + }) + }) +} + func Test_NamedIndexes(t *testing.T) { Wrap(t, func(t *testing.T, client arangodb.Client) { WithDatabase(t, client, nil, func(db arangodb.Database) { diff --git a/v2/tests/util_test.go b/v2/tests/util_test.go index 6860d442..24effd28 100644 --- a/v2/tests/util_test.go +++ b/v2/tests/util_test.go @@ -23,9 +23,12 @@ package tests import ( + "context" "os" "strings" "testing" + + "github.com/arangodb/go-driver/v2/arangodb" ) func getTestMode() string { @@ -57,3 +60,14 @@ func requireSingleMode(t *testing.T) { func requireResilientSingleMode(t *testing.T) { requireMode(t, testModeResilientSingle) } + +func skipBelowVersion(c arangodb.Client, ctx context.Context, version arangodb.Version, t *testing.T) arangodb.VersionInfo { + x, err := c.Version(ctx) + if err != nil { + t.Fatalf("Failed to get version info: %s", err) + } + if x.Version.CompareTo(version) < 0 { + t.Skipf("Skipping below version '%s', got version '%s'", version, x.Version) + } + return x +} From 9c3109fe4cc04decbac47cf1db89c6b384009f3a Mon Sep 17 00:00:00 2001 From: jwierzbo Date: Tue, 14 Feb 2023 07:36:34 +0100 Subject: [PATCH 06/14] readme update --- v2/README.md | 27 ++++++++++---------- v2/tests/database_collection_indexes_test.go | 23 ++++++++++++++++- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/v2/README.md b/v2/README.md index a529b35e..e744d78d 100644 --- a/v2/README.md +++ b/v2/README.md @@ -4,16 +4,17 @@ Implementation of Driver V2 make use of runtime JSON/VPACK serialization, reduci ## Features -| Implemented | Test Coverage | Description | -|-------------|---------------|-------------------------------------------------------------------------| -| ✓ | ✓ | HTTP JSON & VPACK Connection | -| - | - | HTTP2 JSON & VPACK Connection | -| - | - | VST Connection | -| + | - | Database API Implementation | -| + | - | Collection API Implementation | -| ✓ | ✓ | Collection Document Creation | -| + | - | Collection Document Update | -| ✓ | ✓ | Collection Document Read | -| ✓ | ✓ | Query Execution | -| + | - | Transaction Execution | -| - | - | ArangoDB Operations (Views, Users, Graphs) | \ No newline at end of file +| Implemented | Test Coverage | Description | +|-------------|---------------|--------------------------------------------| +| ✓ | ✓ | HTTP JSON & VPACK Connection | +| - | - | HTTP2 JSON & VPACK Connection | +| - | - | VST Connection | +| + | - | Database API Implementation | +| + | - | Collection API Implementation | +| ✓ | ✓ | Collection Document Creation | +| + | - | Collection Document Update | +| ✓ | ✓ | Collection Document Read | +| ✓ | ✓ | Collection Index | +| ✓ | ✓ | Query Execution | +| + | - | Transaction Execution | +| - | - | ArangoDB Operations (Views, Users, Graphs) | \ No newline at end of file diff --git a/v2/tests/database_collection_indexes_test.go b/v2/tests/database_collection_indexes_test.go index 6a7a7e3b..3b42cfbe 100644 --- a/v2/tests/database_collection_indexes_test.go +++ b/v2/tests/database_collection_indexes_test.go @@ -81,7 +81,7 @@ func Test_DefaultEdgeIndexes(t *testing.T) { }) } -func Test_EnsurePersistentIndexBasicOpts(t *testing.T) { +func Test_EnsurePersistentIndex(t *testing.T) { Wrap(t, func(t *testing.T, client arangodb.Client) { WithDatabase(t, client, nil, func(db arangodb.Database) { WithCollection(t, db, nil, func(col arangodb.Collection) { @@ -139,6 +139,27 @@ func Test_EnsurePersistentIndexBasicOpts(t *testing.T) { })) } + t.Run("Create Persistent index with Cache", func(t *testing.T) { + skipBelowVersion(client, ctx, "3.10", t) + + fields := []string{"year", "type"} + storedValues := []string{"extra1", "extra2"} + + options := &arangodb.CreatePersistentIndexOptions{ + StoredValues: storedValues, + CacheEnabled: newBool(true), + } + + idx, created, err := col.EnsurePersistentIndex(ctx, fields, options) + require.NoError(t, err) + require.True(t, created) + require.Equal(t, arangodb.PersistentIndexType, idx.Type) + require.True(t, *idx.RegularIndex.CacheEnabled) + assert.ElementsMatch(t, idx.RegularIndex.Fields, fields) + assert.ElementsMatch(t, idx.RegularIndex.StoredValues, storedValues) + + }) + return nil }) }) From 8126531447cd1c660f5372f4cc753cda24bc3240 Mon Sep 17 00:00:00 2001 From: jwierzbo Date: Wed, 15 Feb 2023 13:56:16 +0100 Subject: [PATCH 07/14] Update v2/arangodb/collection_indexes.go Co-authored-by: Nikita Vaniasin --- v2/arangodb/collection_indexes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2/arangodb/collection_indexes.go b/v2/arangodb/collection_indexes.go index a374cd74..801ce94e 100644 --- a/v2/arangodb/collection_indexes.go +++ b/v2/arangodb/collection_indexes.go @@ -224,7 +224,7 @@ type CreatePersistentIndexOptions struct { // regardless of the value of this attribute. Deduplicate *bool `json:"deduplicate,omitempty"` - // Estimates determines if the to-be-created index should maintain selectivity estimates or not. + // Estimates determines if the to-be-created index should maintain selectivity estimates or not. // Is supported by indexes of type persistent // This attribute controls whether index selectivity estimates are maintained for the index. // Not maintaining index selectivity estimates can have a slightly positive impact on write performance. From 6071d2e0b4848bc3aab66dfbbb2e1bd7fa5eecc7 Mon Sep 17 00:00:00 2001 From: jwierzbo Date: Wed, 15 Feb 2023 13:56:26 +0100 Subject: [PATCH 08/14] Update v2/arangodb/collection_indexes.go Co-authored-by: Nikita Vaniasin --- v2/arangodb/collection_indexes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2/arangodb/collection_indexes.go b/v2/arangodb/collection_indexes.go index 801ce94e..e8eb9764 100644 --- a/v2/arangodb/collection_indexes.go +++ b/v2/arangodb/collection_indexes.go @@ -75,7 +75,7 @@ type CollectionIndexes interface { DeleteIndex(ctx context.Context, name string) error // DeleteIndexById deletes an index from the collection. - DeleteIndexById(ctx context.Context, id string) error + DeleteIndexByID(ctx context.Context, id string) error } // IndexType represents an index type as string From e67e024f82c11da1509ca94be9446163210990dd Mon Sep 17 00:00:00 2001 From: jwierzbo Date: Wed, 15 Feb 2023 15:34:41 +0100 Subject: [PATCH 09/14] GT-336 inverted index test --- v2/arangodb/collection_indexe_inverted.go | 4 +- v2/arangodb/collection_indexes.go | 2 +- v2/arangodb/collection_indexes_impl.go | 4 +- v2/arangodb/shared.go | 6 +- ...tabase_collection_indexes_inverted_test.go | 134 ++++++++++++++++++ v2/tests/database_collection_indexes_test.go | 6 +- v2/tests/util_test.go | 10 ++ view_arangosearch.go | 6 +- 8 files changed, 159 insertions(+), 13 deletions(-) create mode 100644 v2/tests/database_collection_indexes_inverted_test.go diff --git a/v2/arangodb/collection_indexe_inverted.go b/v2/arangodb/collection_indexe_inverted.go index 93d6d140..dbff2975 100644 --- a/v2/arangodb/collection_indexe_inverted.go +++ b/v2/arangodb/collection_indexe_inverted.go @@ -70,7 +70,7 @@ type InvertedIndexOptions struct { IncludeAllFields *bool `json:"includeAllFields,omitempty"` // TrackListPositions track the value position in arrays for array values. - TrackListPositions *bool `json:"trackListPositions,omitempty"` + TrackListPositions bool `json:"trackListPositions,omitempty"` // Parallelism - The number of threads to use for indexing the fields. Default: 2 Parallelism *int `json:"parallelism,omitempty"` @@ -130,7 +130,7 @@ type InvertedIndexField struct { // If set to true, then track the value position in arrays for array values. For example, when querying a document like { attr: [ "valueX", "valueY", "valueZ" ] }, you need to specify the array element, e.g. doc.attr[1] == "valueY". // If set to false, all values in an array are treated as equal alternatives. You don’t specify an array element in queries, e.g. doc.attr == "valueY", and all elements are searched for a match. // Default: the value defined by the top-level trackListPositions option, or false if not set. - TrackListPositions *bool `json:"trackListPositions,omitempty"` + TrackListPositions bool `json:"trackListPositions,omitempty"` // Cache - Enable this option to always cache the field normalization values in memory for this specific field // Default: the value defined by the top-level 'cache' option. diff --git a/v2/arangodb/collection_indexes.go b/v2/arangodb/collection_indexes.go index e8eb9764..e7c946fc 100644 --- a/v2/arangodb/collection_indexes.go +++ b/v2/arangodb/collection_indexes.go @@ -74,7 +74,7 @@ type CollectionIndexes interface { // DeleteIndex deletes an index from the collection. DeleteIndex(ctx context.Context, name string) error - // DeleteIndexById deletes an index from the collection. + // DeleteIndexByID deletes an index from the collection. DeleteIndexByID(ctx context.Context, id string) error } diff --git a/v2/arangodb/collection_indexes_impl.go b/v2/arangodb/collection_indexes_impl.go index b24c98a5..66083a80 100644 --- a/v2/arangodb/collection_indexes_impl.go +++ b/v2/arangodb/collection_indexes_impl.go @@ -224,10 +224,10 @@ func (c *collectionIndexes) DeleteIndex(ctx context.Context, name string) error return errors.WithStack(err) } - return c.DeleteIndexById(ctx, idx.ID) + return c.DeleteIndexByID(ctx, idx.ID) } -func (c *collectionIndexes) DeleteIndexById(ctx context.Context, id string) error { +func (c *collectionIndexes) DeleteIndexByID(ctx context.Context, id string) error { url := c.collection.db.url("_api", "index", id) response := shared.ResponseStruct{} diff --git a/v2/arangodb/shared.go b/v2/arangodb/shared.go index f4858c2b..ee5057e0 100644 --- a/v2/arangodb/shared.go +++ b/v2/arangodb/shared.go @@ -60,10 +60,10 @@ type PrimarySort struct { // PrimarySortEntry field to sort the index by and the direction type PrimarySortEntry struct { // Field An attribute path. The . character denotes sub-attributes. - Field string `json:"field,omitempty"` + Field string `json:"field,required"` - // Direction The sorting direction - Direction *SortDirection `json:"direction,omitempty"` + // Ascending The sorting direction + Ascending bool `json:"asc,required"` } // StoredValue defines the value stored in the index diff --git a/v2/tests/database_collection_indexes_inverted_test.go b/v2/tests/database_collection_indexes_inverted_test.go new file mode 100644 index 00000000..a91ed821 --- /dev/null +++ b/v2/tests/database_collection_indexes_inverted_test.go @@ -0,0 +1,134 @@ +// +// DISCLAIMER +// +// Copyright 2023 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Jakub Wierzbowski +// + +package tests + +import ( + "context" + "github.com/stretchr/testify/assert" + "golang.org/x/exp/slices" + "testing" + "time" + + "github.com/arangodb/go-driver/v2/arangodb" + "github.com/stretchr/testify/require" +) + +func Test_EnsureInvertedIndex(t *testing.T) { + Wrap(t, func(t *testing.T, client arangodb.Client) { + WithDatabase(t, client, nil, func(db arangodb.Database) { + WithCollection(t, db, nil, func(col arangodb.Collection) { + withContext(30*time.Second, func(ctx context.Context) error { + + type testCase struct { + IsEE bool + Options arangodb.InvertedIndexOptions + } + + testCases := []testCase{ + { + IsEE: false, + Options: arangodb.InvertedIndexOptions{ + Name: "inverted-opt", + PrimarySort: &arangodb.PrimarySort{ + Fields: []arangodb.PrimarySortEntry{ + {Field: "test1", Ascending: true}, + {Field: "test2", Ascending: false}, + }, + Compression: arangodb.PrimarySortCompressionLz4, + }, + Fields: []arangodb.InvertedIndexField{ + {Name: "test1", Features: []arangodb.AnalyzerFeature{arangodb.AnalyzerFeatureFrequency}, Nested: nil}, + {Name: "test2", Features: []arangodb.AnalyzerFeature{arangodb.AnalyzerFeatureFrequency, arangodb.AnalyzerFeaturePosition}, Nested: nil}, + }, + }, + }, + { + IsEE: true, + Options: arangodb.InvertedIndexOptions{ + Name: "inverted-opt-nested", + PrimarySort: &arangodb.PrimarySort{ + Fields: []arangodb.PrimarySortEntry{ + {Field: "test1", Ascending: true}, + {Field: "test2", Ascending: false}, + }, + Compression: arangodb.PrimarySortCompressionLz4, + }, + Fields: []arangodb.InvertedIndexField{ + {Name: "field1", Features: []arangodb.AnalyzerFeature{arangodb.AnalyzerFeatureFrequency}, Nested: nil}, + {Name: "field2", Features: []arangodb.AnalyzerFeature{arangodb.AnalyzerFeatureFrequency, arangodb.AnalyzerFeaturePosition}, + Nested: []arangodb.InvertedIndexNestedField{ + { + Name: "some-nested-field", + Nested: []arangodb.InvertedIndexNestedField{ + {Name: "test"}, + {Name: "bas", Nested: []arangodb.InvertedIndexNestedField{ + {Name: "a", Features: nil}, + }}, + {Name: "kas", Nested: []arangodb.InvertedIndexNestedField{ + {Name: "c"}, + }}, + }, + }, + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.Options.Name, func(t *testing.T) { + if tc.IsEE { + skipNoEnterprise(client, ctx, t) + } + + idx, created, err := col.EnsureInvertedIndex(ctx, &tc.Options) + require.NoError(t, err) + require.True(t, created) + + requireIdxEquality := func(invertedIdx arangodb.IndexResponse) { + require.Equal(t, arangodb.InvertedIndexType, idx.Type) + require.Equal(t, tc.Options.Name, idx.Name) + require.Equal(t, tc.Options.PrimarySort, idx.InvertedIndex.PrimarySort) + require.Equal(t, tc.Options.Fields, idx.InvertedIndex.Fields) + } + requireIdxEquality(idx) + + indexes, err := col.Indexes(ctx) + require.NoError(t, err) + require.NotNil(t, indexes) + assert.True(t, slices.ContainsFunc(indexes, func(i arangodb.IndexResponse) bool { + return i.ID == idx.ID && i.Name == tc.Options.Name + })) + + err = col.DeleteIndex(ctx, idx.Name) + require.NoError(t, err) + }) + } + + return nil + }) + }) + }) + }) +} diff --git a/v2/tests/database_collection_indexes_test.go b/v2/tests/database_collection_indexes_test.go index 3b42cfbe..672130d2 100644 --- a/v2/tests/database_collection_indexes_test.go +++ b/v2/tests/database_collection_indexes_test.go @@ -193,7 +193,7 @@ func Test_EnsurePersistentIndexDeduplicate(t *testing.T) { require.Error(t, err) require.True(t, shared.IsConflict(err)) - err = col.DeleteIndexById(ctx, idx.ID) + err = col.DeleteIndexByID(ctx, idx.ID) require.NoError(t, err) }) @@ -261,7 +261,7 @@ func Test_TTLIndex(t *testing.T) { return nil }) - err = col.DeleteIndexById(ctx, idx.ID) + err = col.DeleteIndexByID(ctx, idx.ID) require.NoError(t, err) }) @@ -298,7 +298,7 @@ func Test_TTLIndex(t *testing.T) { return nil }) - err = col.DeleteIndexById(ctx, idx.ID) + err = col.DeleteIndexByID(ctx, idx.ID) require.NoError(t, err) }) diff --git a/v2/tests/util_test.go b/v2/tests/util_test.go index 24effd28..d600fe25 100644 --- a/v2/tests/util_test.go +++ b/v2/tests/util_test.go @@ -29,6 +29,7 @@ import ( "testing" "github.com/arangodb/go-driver/v2/arangodb" + "github.com/stretchr/testify/require" ) func getTestMode() string { @@ -61,6 +62,15 @@ func requireResilientSingleMode(t *testing.T) { requireMode(t, testModeResilientSingle) } +func skipNoEnterprise(c arangodb.Client, ctx context.Context, t *testing.T) { + version, err := c.Version(ctx) + require.NoError(t, err) + + if !version.IsEnterprise() { + t.Skip("Skipping test, no enterprise version") + } +} + func skipBelowVersion(c arangodb.Client, ctx context.Context, version arangodb.Version, t *testing.T) arangodb.VersionInfo { x, err := c.Version(ctx) if err != nil { diff --git a/view_arangosearch.go b/view_arangosearch.go index b8a5e915..4565dc83 100644 --- a/view_arangosearch.go +++ b/view_arangosearch.go @@ -393,8 +393,10 @@ const ( // ArangoSearchPrimarySortEntry describes an entry for the primarySort list type ArangoSearchPrimarySortEntry struct { - Field string `json:"field,omitempty"` - Ascending *bool `json:"asc,omitempty"` + Field string `json:"field,omitempty"` + Ascending *bool `json:"asc,omitempty"` + + // deprecated, please use Ascending instead Direction *ArangoSearchSortDirection `json:"direction,omitempty"` } From 2e5b68e483a802bafd609c8cb982c053549610a4 Mon Sep 17 00:00:00 2001 From: jwierzbo Date: Wed, 15 Feb 2023 15:46:57 +0100 Subject: [PATCH 10/14] GT-336 inverted index TrackListPositions test --- ...tabase_collection_indexes_inverted_test.go | 44 +++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/v2/tests/database_collection_indexes_inverted_test.go b/v2/tests/database_collection_indexes_inverted_test.go index a91ed821..ffcdae45 100644 --- a/v2/tests/database_collection_indexes_inverted_test.go +++ b/v2/tests/database_collection_indexes_inverted_test.go @@ -57,11 +57,41 @@ func Test_EnsureInvertedIndex(t *testing.T) { Compression: arangodb.PrimarySortCompressionLz4, }, Fields: []arangodb.InvertedIndexField{ - {Name: "test1", Features: []arangodb.AnalyzerFeature{arangodb.AnalyzerFeatureFrequency}, Nested: nil}, - {Name: "test2", Features: []arangodb.AnalyzerFeature{arangodb.AnalyzerFeatureFrequency, arangodb.AnalyzerFeaturePosition}, Nested: nil}, + { + Name: "test1", + Features: []arangodb.AnalyzerFeature{arangodb.AnalyzerFeatureFrequency}, + Nested: nil}, + { + Name: "test2", + Features: []arangodb.AnalyzerFeature{arangodb.AnalyzerFeatureFrequency, arangodb.AnalyzerFeaturePosition}, + Nested: nil}, }, }, }, + { + IsEE: false, + Options: arangodb.InvertedIndexOptions{ + Name: "inverted-overwrite-tracklistpositions", + PrimarySort: &arangodb.PrimarySort{ + Fields: []arangodb.PrimarySortEntry{ + {Field: "test1", Ascending: true}, + {Field: "test2", Ascending: false}, + }, + Compression: arangodb.PrimarySortCompressionLz4, + }, + Fields: []arangodb.InvertedIndexField{ + { + Name: "test1-overwrite", + Features: []arangodb.AnalyzerFeature{arangodb.AnalyzerFeatureFrequency}, + Nested: nil, TrackListPositions: true}, + { + Name: "test2", + Features: []arangodb.AnalyzerFeature{arangodb.AnalyzerFeatureFrequency, arangodb.AnalyzerFeaturePosition}, + Nested: nil}, + }, + TrackListPositions: false, + }, + }, { IsEE: true, Options: arangodb.InvertedIndexOptions{ @@ -74,8 +104,13 @@ func Test_EnsureInvertedIndex(t *testing.T) { Compression: arangodb.PrimarySortCompressionLz4, }, Fields: []arangodb.InvertedIndexField{ - {Name: "field1", Features: []arangodb.AnalyzerFeature{arangodb.AnalyzerFeatureFrequency}, Nested: nil}, - {Name: "field2", Features: []arangodb.AnalyzerFeature{arangodb.AnalyzerFeatureFrequency, arangodb.AnalyzerFeaturePosition}, + { + Name: "field1", + Features: []arangodb.AnalyzerFeature{arangodb.AnalyzerFeatureFrequency}, + Nested: nil}, + { + Name: "field2", + Features: []arangodb.AnalyzerFeature{arangodb.AnalyzerFeatureFrequency, arangodb.AnalyzerFeaturePosition}, Nested: []arangodb.InvertedIndexNestedField{ { Name: "some-nested-field", @@ -111,6 +146,7 @@ func Test_EnsureInvertedIndex(t *testing.T) { require.Equal(t, tc.Options.Name, idx.Name) require.Equal(t, tc.Options.PrimarySort, idx.InvertedIndex.PrimarySort) require.Equal(t, tc.Options.Fields, idx.InvertedIndex.Fields) + require.Equal(t, tc.Options.TrackListPositions, idx.InvertedIndex.TrackListPositions) } requireIdxEquality(idx) From 2eeb77e98df13e8fbbc38f2bc853ee39e37e2833 Mon Sep 17 00:00:00 2001 From: jwierzbo Date: Wed, 15 Feb 2023 17:12:26 +0100 Subject: [PATCH 11/14] GT-336 fix fmt errors --- v2/tests/database_collection_indexes_inverted_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/v2/tests/database_collection_indexes_inverted_test.go b/v2/tests/database_collection_indexes_inverted_test.go index ffcdae45..848a402f 100644 --- a/v2/tests/database_collection_indexes_inverted_test.go +++ b/v2/tests/database_collection_indexes_inverted_test.go @@ -24,13 +24,14 @@ package tests import ( "context" - "github.com/stretchr/testify/assert" - "golang.org/x/exp/slices" "testing" "time" - "github.com/arangodb/go-driver/v2/arangodb" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/exp/slices" + + "github.com/arangodb/go-driver/v2/arangodb" ) func Test_EnsureInvertedIndex(t *testing.T) { From 8d9acac4503c7347c5f0f79d882844d73197999e Mon Sep 17 00:00:00 2001 From: jwierzbo Date: Wed, 15 Feb 2023 18:19:19 +0100 Subject: [PATCH 12/14] GT-336 skipBelowVersion for inverted tests --- v2/tests/database_collection_indexes_inverted_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/v2/tests/database_collection_indexes_inverted_test.go b/v2/tests/database_collection_indexes_inverted_test.go index 848a402f..d553ce9b 100644 --- a/v2/tests/database_collection_indexes_inverted_test.go +++ b/v2/tests/database_collection_indexes_inverted_test.go @@ -39,6 +39,7 @@ func Test_EnsureInvertedIndex(t *testing.T) { WithDatabase(t, client, nil, func(db arangodb.Database) { WithCollection(t, db, nil, func(col arangodb.Collection) { withContext(30*time.Second, func(ctx context.Context) error { + skipBelowVersion(client, ctx, "3.10", t) type testCase struct { IsEE bool From a8524db6614dcfa42f4ad6e84a1bc30e78bca561 Mon Sep 17 00:00:00 2001 From: jwierzbo Date: Thu, 16 Feb 2023 06:33:40 +0100 Subject: [PATCH 13/14] GT-336 fix failing TTLIndex tests --- v2/tests/database_collection_indexes_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/v2/tests/database_collection_indexes_test.go b/v2/tests/database_collection_indexes_test.go index 672130d2..ac910652 100644 --- a/v2/tests/database_collection_indexes_test.go +++ b/v2/tests/database_collection_indexes_test.go @@ -226,7 +226,7 @@ func Test_TTLIndex(t *testing.T) { Wrap(t, func(t *testing.T, client arangodb.Client) { WithDatabase(t, client, nil, func(db arangodb.Database) { WithCollection(t, db, nil, func(col arangodb.Collection) { - withContext(120*time.Second, func(ctx context.Context) error { + withContext(240*time.Second, func(ctx context.Context) error { t.Run("Removing documents at a fixed period after creation", func(t *testing.T) { idx, created, err := col.EnsureTTLIndex(ctx, []string{"createdAt"}, 5, nil) @@ -248,8 +248,8 @@ func Test_TTLIndex(t *testing.T) { require.NoError(t, err) require.True(t, exist) - // cleanup is made every 30 seconds by default, so we need to wait for 35 seconds in worst case - withContext(35*time.Second, func(ctx context.Context) error { + // cleanup is made every 30 seconds by default + withContext(65*time.Second, func(ctx context.Context) error { for { exist, err := col.DocumentExists(ctx, meta.Key) require.NoError(t, err) @@ -285,8 +285,8 @@ func Test_TTLIndex(t *testing.T) { require.NoError(t, err) require.True(t, exist) - // cleanup is made every 30 seconds by default, so we need to wait for 35 seconds in worst case - withContext(35*time.Second, func(ctx context.Context) error { + // cleanup is made every 30 seconds by default + withContext(65*time.Second, func(ctx context.Context) error { for { exist, err := col.DocumentExists(ctx, meta.Key) require.NoError(t, err) From 0c2ce612f8a798a44edff784d917796f337235ee Mon Sep 17 00:00:00 2001 From: jwierzbo Date: Thu, 16 Feb 2023 08:06:52 +0100 Subject: [PATCH 14/14] GT-336 skip inverted index tests below 3.10 --- v2/tests/database_collection_indexes_test.go | 29 +++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/v2/tests/database_collection_indexes_test.go b/v2/tests/database_collection_indexes_test.go index ac910652..a11172cc 100644 --- a/v2/tests/database_collection_indexes_test.go +++ b/v2/tests/database_collection_indexes_test.go @@ -24,6 +24,7 @@ package tests import ( "context" + "fmt" "testing" "time" @@ -390,6 +391,7 @@ func Test_NamedIndexes(t *testing.T) { var namedIndexTestCases = []struct { Name string CreateCallback func(col arangodb.Collection, name string) (arangodb.IndexResponse, error) + MinVersion arangodb.Version }{ { Name: "Persistent", @@ -429,7 +431,8 @@ func Test_NamedIndexes(t *testing.T) { }, }, { - Name: "Inverted", + Name: "Inverted", + MinVersion: "3.10", CreateCallback: func(col arangodb.Collection, name string) (arangodb.IndexResponse, error) { idx, _, err := col.EnsureInvertedIndex(ctx, &arangodb.InvertedIndexOptions{ Name: name, @@ -445,16 +448,22 @@ func Test_NamedIndexes(t *testing.T) { } for _, testCase := range namedIndexTestCases { - idx, err := testCase.CreateCallback(col, testCase.Name) - require.NoError(t, err) - require.Equal(t, testCase.Name, idx.Name) + t.Run(fmt.Sprintf("Test named index: %s", testCase.Name), func(t *testing.T) { + if testCase.MinVersion != "" { + skipBelowVersion(client, ctx, testCase.MinVersion, t) + } - indexes, err := col.Indexes(ctx) - require.NoError(t, err) - require.NotNil(t, indexes) - assert.True(t, slices.ContainsFunc(indexes, func(i arangodb.IndexResponse) bool { - return i.ID == idx.ID && i.Name == testCase.Name - })) + idx, err := testCase.CreateCallback(col, testCase.Name) + require.NoError(t, err) + require.Equal(t, testCase.Name, idx.Name) + + indexes, err := col.Indexes(ctx) + require.NoError(t, err) + require.NotNil(t, indexes) + assert.True(t, slices.ContainsFunc(indexes, func(i arangodb.IndexResponse) bool { + return i.ID == idx.ID && i.Name == testCase.Name + })) + }) } return nil